diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 00000000..410c77c0 --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 03e2ae4d3d323123e0cb813c983c6d98 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/API/core.html b/API/core.html new file mode 100644 index 00000000..3b555071 --- /dev/null +++ b/API/core.html @@ -0,0 +1,3049 @@ + + + + + + + + + + + Simulation Core Classes — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Simulation Core Classes

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Simulation Core Classes#

+

There are a variety of components that enable a simulation to be run, from the +environment to the management of repairs to the servicing equipment. The below will show +how each of the APIs are powered to enable the full flexibility of modeling.

+ +
+

Environment#

+
+
+class wombat.core.environment.WombatEnvironment(data_dir, weather_file, workday_start, workday_end, simulation_name=None, start_year=None, end_year=None, port_distance=None, non_operational_start=None, non_operational_end=None, reduced_speed_start=None, reduced_speed_end=None, reduced_speed=0.0, random_seed=None, random_generator=None)[source]#
+

The primary mechanism for powering an O&M simulation. This object has insight +into all other simulation objects, and controls the timing, date/time stamps, and +weather conditions.

+
+
Parameters:
+
    +
  • data_dir (pathlib.Path | str) -- Directory where the inputs are stored and where to save outputs.

  • +
  • weather_file (str) -- Name of the weather file. Should be contained within data_dir/weather/, with +columns "datetime", "windspeed", and, optionally, "waveheight". The datetime +column should adhere to the following format: "MM/DD/YY HH:MM", in 24-hour time.

  • +
  • workday_start (int) -- Starting time for the repair crew, in 24 hour local time. This can be overridden +by an ServiceEquipmentData object that operates outside of the "typical" +working hours.

  • +
  • workday_end (int) -- Ending time for the repair crew, in 24 hour local time. This can be overridden +by an ServiceEquipmentData object that operates outside of the "typical" +working hours.

  • +
  • simulation_name (str | None, optional) --

    Name of the simulation; will be used for naming the log file, by default None. +If None, then the current time will be used. Will always save to +data_dir/outputs/logs/simulation_name.log.

    +

  • +
  • start_year (int | None, optional) -- Custom starting year for the weather profile, by default None. If None or +less than the first year of the weather profile, this will be ignored.

  • +
  • end_year (int | None, optional) -- Custom ending year for the weather profile, by default None. If None or +greater than the last year of the weather profile, this will be ignored.

  • +
  • port_distance (int | float) -- The simulation-wide daily travel distance for servicing equipment. This +should be used as a base setting when multiple or all servicing equipment +will be operating out of the same base location, but can be individually +modified.

  • +
  • non_operational_start (str | datetime.datetime | None) -- The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of prohibited operations. When defined at the environment level, +an undefined or later starting date will be overridden for all servicing +equipment and any modeled port, by default None.

  • +
  • non_operational_end (str | datetime.datetime | None) -- The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of prohibited operations. When defined at the environment level, +an undefined or earlier ending date will be overridden for all servicing +equipment and any modeled port, by default None.

  • +
  • reduced_speed_start (str | datetime.datetime | None) -- The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of reduced speed operations. When defined at the environment level, +an undefined or later starting date will be overridden for all servicing +equipment and any modeled port, by default None.

  • +
  • reduced_speed_end (str | datetime.datetime | None) -- The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of reduced speed operations. When defined at the environment level, +an undefined or earlier ending date will be overridden for all servicing +equipment and any modeled port, by default None.

  • +
  • reduced_speed (float) -- The maximum operating speed during the annualized reduced speed operations. +When defined at the environment level, an undefined or faster value will be +overridden for all servicing equipment and any modeled port, by default 0.0.

  • +
  • random_seed (int | None) -- The random seed to be passed to a universal NumPy default_rng object to +generate Weibull random generators, by default None.

  • +
  • random_generator (np.random._generator.Generator | None) -- An optional numpy random generator that can be provided to seed a simulation +with the same generator each time, in place of the random seed. If a +random_seed is also provided, this will override the random seed, +by default None.

  • +
+
+
Raises:
+

FileNotFoundError -- Raised if data_dir cannot be found.

+
+
+
+
+_register_windfarm(windfarm)[source]#
+

Adds the simulation windfarm to the class attributes.

+
+
Return type:
+

None

+
+
Parameters:
+

windfarm (Windfarm) --

+
+
+
+ +
+
+run(until=None)[source]#
+

Extends the simpy.Environment.run method to change the default behavior +if no argument is passed to until, which will now run a simulation until the +end of the weather profile is reached.

+
+
Parameters:
+

until (Optional[Union[int, float, Event]], optional) -- When to stop the simulation, by default None. See documentation on +simpy.Environment.run for more details.

+
+
+
+ +
+
+_logging_setup()[source]#
+

Completes the setup for logging data.

+
+
Return type:
+

None

+
+
+
+ +
+
+get_random_seconds(low=0, high=10)[source]#
+

Generate a random number of seconds to wait, between low and +high.

+
+
Parameters:
+
    +
  • low (int, optional) -- Minimum number of seconds to wait, by default 0.

  • +
  • high (int, optional) -- Maximum number of seconds to wait, by default 10.

  • +
+
+
Return type:
+

float

+
+
Returns:
+

float -- Number of seconds to wait.

+
+
+
+ +
+
+property simulation_time: datetime#
+

Current time within the simulation ("datetime" column within weather).

+
+ +
+
+is_workshift(workday_start=-1, workday_end=-1)[source]#
+

Check if the current simulation time is within the windfarm's working hours.

+
+
Parameters:
+
    +
  • workday_start (int) -- A valid hour in 24 hour time, by default -1. This should only be provided +from an ServiceEquipmentData object. workday_end must also be +provided in order to be used.

  • +
  • workday_end (int) -- A valid hour in 24 hour time, by default -1. This should only be provided +from an ServiceEquipmentData object. workday_start must also be +provided in order to be used.

  • +
+
+
Return type:
+

bool

+
+
Returns:
+

bool -- True if it's valid working hours, False otherwise.

+
+
+
+ +
+
+hour_in_shift(hour, workday_start=-1, workday_end=-1)[source]#
+

Checks whether an hour is within the working hours.

+
+
Parameters:
+
    +
  • hour (int) -- Hour of the day.

  • +
  • workday_start (int) -- A valid hour in 24 hour time, by default -1. This should only be provided +from an ServiceEquipmentData object. workday_end must also be +provided in order to be used.

  • +
  • workday_end (int) -- A valid hour in 24 hour time, by default -1. This should only be provided +from an ServiceEquipmentData object. workday_start must also be +provided in order to be used.

  • +
+
+
Return type:
+

bool

+
+
Returns:
+

bool -- True if hour is during working hours, False otherwise.

+
+
+
+ +
+
+hours_to_next_shift(workday_start=-1)[source]#
+

Time until the next work shift starts, in hours.

+
+
Parameters:
+

workday_start (int) -- A valid hour in 24 hour time, by default -1. This should only be provided +from an ServiceEquipmentData object.

+
+
Return type:
+

float

+
+
Returns:
+

float -- Hours until the next shift starts.

+
+
+
+ +
+
+property current_time: str#
+

Timestamp for the current time as a datetime.datetime.strftime.

+
+ +
+
+date_ix(date)[source]#
+

The first index of a future date. This corresponds to the number of hours +until this dates from the very beginning of the simulation.

+
+
Parameters:
+

date (datetime.datetime | datetime.date) -- A date within the environment's simulation range.

+
+
Return type:
+

int

+
+
Returns:
+

int -- Index of the weather profile corresponds to the first hour of date.

+
+
+
+ +
+
+_weather_setup(weather_file, start_year=None, end_year=None)[source]#
+

Reads the weather data from the "<inputs>/weather" directory, and creates the +start_date and end_date time stamps for the simulation.

+

This also fills any missing data with zeros and interpolates the values of any +missing datetime entries.

+
+
Parameters:
+
    +
  • weather_file (str) -- Name of the weather file to be used by the environment. Should be contained +within data_dir/weather.

  • +
  • start_year (Optional[int], optional) -- Custom starting year for the weather profile, by default None. If None +or less than the first year of the weather profile, this will be ignored.

  • +
  • end_year (Optional[int], optional) -- Custom ending year for the weather profile, by default None. If None or +greater than the last year of the weather profile, this will be ignored.

  • +
+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- The wind (and wave) timeseries.

+
+
+
+ +
+
+property weather_now: DataFrame#
+

The current weather.

+
+
Returns:
+

pl.DataFrame -- A length 1 slice from the weather profile at the current int() rounded +hour, in simulation time.

+
+
+
+ +
+
+weather_forecast(hours)[source]#
+

Returns the datetime, wind, wave, and hour data for the next hours hours, +starting from the current hour's weather.

+
+
Parameters:
+

hours (Union[int, float]) -- Number of hours to look ahead, rounds up to the nearest hour.

+
+
Return type:
+

tuple[Series, Series, Series, Series]

+
+
Returns:
+

tuple[pl.Series, pl.Series, pl.Series, pl.Series] -- Each of the relevant columns (datetime, wind, wave, hour) from the weather +profile.

+
+
+
+ +
+
+log_action(*, agent, action, reason, additional='', system_id='', system_name='', part_id='', part_name='', system_ol=0, part_ol=0, duration=0, distance_km=0, request_id='na', location='na', materials_cost=0, hourly_labor_cost=0, salary_labor_cost=0, equipment_cost=0)[source]#
+

Formats the logging messages into the expected format for logging.

+
+
Parameters:
+
    +
  • agent (str) -- Agent performing the action.

  • +
  • action (str) -- Action that was taken.

  • +
  • reason (str) -- Reason an action was taken.

  • +
  • additional (str) -- Any additional information that needs to be logged.

  • +
  • system_id (str) -- Turbine ID, System.id, by default "".

  • +
  • system_name (str) -- Turbine name, System.name, by default "".

  • +
  • part_id (str) -- Subassembly, component, or cable ID, _.id, by default "".

  • +
  • part_name (str) -- Subassembly, component, or cable name, _.name, by default "".

  • +
  • system_ol (float | int) -- Turbine operating level, System.operating_level. Use an empty string +for n/a, by default 0.

  • +
  • part_ol (float | int) -- Subassembly, component, or cable operating level, _.operating_level. Use +an empty string for n/a, by default 0.

  • +
  • request_id (str) -- The RepairManager assigned request_id found in +RepairRequest.request_id, by default "na".

  • +
  • location (str) -- The location of where the event ocurred: should be one of site, port, +enroute, or system, by default "na".

  • +
  • duration (float) -- Length of time the action lasted, by default 0.

  • +
  • distance (float) -- Distance traveled, in km, if applicable, by default 0.

  • +
  • materials_cost (Union[int, float], optional) -- Total cost of materials for action, in USD, by default 0.

  • +
  • hourly_labor_cost (Union[int, float], optional) -- Total cost of hourly labor for action, in USD, by default 0.

  • +
  • salary_labor_cost (Union[int, float], optional) -- Total cost of salaried labor for action, in USD, by default 0.

  • +
  • equipment_cost (Union[int, float], optional) -- Total cost of equipment for action, in USD, by default 0.

  • +
  • distance_km (float) --

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+_log_actions()[source]#
+

Writes the action log items every 8000 hours.

+
+ +
+
+load_events_log_dataframe()[source]#
+

Imports the logging file created in run and returns it as a formatted +pandas.DataFrame.

+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- The formatted logging data from a simulation.

+
+
+
+ +
+
+_calculate_windfarm_total(op, prod=None)[source]#
+

Calculates the overall wind farm operational level, accounting for substation +downtime by multiplying the sum of all downstream turbine operational levels by +the substation's operational level.

+
+
Parameters:
+
    +
  • op (pd.DataFrame) -- The turbine and substation operational level DataFrame.

  • +
  • prod (DataFrame | None) --

  • +
+
+
Return type:
+

DataFrame

+
+
+
+

Notes

+

This is a crude cap on the operations, and so a smarter way of capping +the availability should be added in the future.

+
+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- The aggregate wind farm operational level.

+
+
Parameters:
+
    +
  • op (DataFrame) --

  • +
  • prod (DataFrame | None) --

  • +
+
+
+
+ +
+
+_calculate_adjusted_production(op, prod)[source]#
+

Calculates the overall wind farm power production and adjusts individual +turbine production by accounting for substation downtime. This is done by +multiplying the all downstream turbine operational levels by the substation's +operational level.

+
+
Parameters:
+
    +
  • op (pd.DataFrame) -- The operational level DataFrame with turbine, substation, and windfarm +columns.

  • +
  • prod (pd.DataFrame) -- The turbine energy production DataFrame.

  • +
+
+
Return type:
+

DataFrame

+
+
+
+

Notes

+

This is a crude cap on the operations, and so a smarter way of capping +the availability should be added in the future.

+
+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- Either the aggregate wind farm operational level or the total wind farm +energy production if the prod is provided.

+
+
Parameters:
+
    +
  • op (DataFrame) --

  • +
  • prod (DataFrame) --

  • +
+
+
+
+ +
+
+load_operations_log_dataframe()[source]#
+

Imports the logging file created in run and returns it as a formatted +pandas.DataFrame.

+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- The formatted logging data from a simulation.

+
+
+
+ +
+
+power_production_potential_to_csv(windfarm, operations=None, return_df=True)[source]#
+

Creates the power production DataFrame and optionally returns it.

+
+
Parameters:
+
    +
  • windfarm (wombat.windfarm.Windfarm) -- The simulation's windfarm object.

  • +
  • operations (Optional[pd.DataFrame], optional) -- The operations log DataFrame if readily available, by default None. If +None, then it will be created through +load_operations_log_dataframe().

  • +
  • return_df (bool, optional) -- Indicator to return the power production for further usage, by default True.

  • +
+
+
Return type:
+

tuple[DataFrame, DataFrame]

+
+
Returns:
+

Tuple[pd.DataFrame, pd.DataFrame] -- The power potential and production timeseries data.

+
+
+
+ +
+
+cleanup_log_files()[source]#
+

Convenience method to clear the output log files in case a large +batch of simulations is being run and there are space limitations.

+
+
Return type:
+

None

+
+
+
+
... warning:: This shuts down the loggers, so no more logging will be able

to be performed.

+
+
+
+ +
+ +
+
+

Repair Management#

+
+
+class wombat.core.repair_management.RepairManager(env, capacity=inf)[source]#
+

Provides a class to manage repair and maintenance tasks.

+
+
Parameters:
+
    +
  • FilterStore (simpy.resources.store.FilterStore) -- The simpy class on which RepairManager is based to manage the repair and +maintenance tasks.

  • +
  • env (wombat.core.WombatEnvironment) -- The simulation environment.

  • +
  • capacity (float) -- The maximum number of tasks that can be submitted to the manager, by default +np.inf.

  • +
+
+
+
+
+env#
+

The simulation environment.

+
+
Type:
+

wombat.core.WombatEnvironment

+
+
+
+ +
+
+windfarm#
+

The simulated windfarm. This is only used for getting the operational capacity.

+
+
Type:
+

wombat.windfarm.Windfarm

+
+
+
+ +
+
+_current_id#
+

The logged and auto-incrememented integer base for the ID generated for each +submitted repair request.

+
+
Type:
+

int

+
+
+
+ +
+
+downtime_based_equipment#
+

The mapping between downtime-based servicing equipment and their capabilities.

+
+
Type:
+

StrategyMap

+
+
+
+ +
+
+request_based_equipment#
+

The mapping between request-based servicing equipment and their capabilities.

+
+
Type:
+

StrategyMap

+
+
+
+ +
+
+_update_equipment_map(service_equipment)[source]#
+

Updates equipment_map with a provided servicing equipment object.

+
+
Return type:
+

None

+
+
Parameters:
+

service_equipment (ServiceEquipment) --

+
+
+
+ +
+
+_register_windfarm(windfarm)[source]#
+

Adds the simulation windfarm to the class attributes.

+
+
Return type:
+

None

+
+
Parameters:
+

windfarm (Windfarm) --

+
+
+
+ +
+
+_register_equipment(service_equipment)[source]#
+

Adds the servicing equipment to the class attributes and adds it to the +capabilities mapping.

+
+
Return type:
+

None

+
+
Parameters:
+

service_equipment (ServiceEquipment) --

+
+
+
+ +
+
+_register_port(port)[source]#
+

Registers the port with the repair manager, so that they can communicate as +needed.

+
+
Parameters:
+

port (Port) -- The port where repairs will occur.

+
+
Return type:
+

None

+
+
+
+ +
+
+_create_request_id(request)[source]#
+

Creates a unique request_id to be logged in the request.

+
+
Parameters:
+

request (RepairRequest) -- The request object.

+
+
Return type:
+

str

+
+
Returns:
+

str -- An 11-digit identifier starting with "MNT" for maintenance tasks or "RPR" +for repairs.

+
+
Raises:
+

ValueError -- If the request.details property is not a Failure or Maintenance + object, + then a ValueError will be raised.

+
+
+
+ +
+
+_is_request_processing(request)[source]#
+

Checks if a repair is being performed, or has already been completed.

+
+
Parameters:
+

request (RepairRequest) -- The request that is about to be submitted to servicing equipment, but needs +to be double-checked against ongoing processes.

+
+
Return type:
+

bool

+
+
Returns:
+

bool -- True if the request is ongoing or completed, False, if it's ok to processed +with the operation.

+
+
+
+ +
+
+_run_equipment_downtime(request)[source]#
+

Run any equipment that has a pending request where the current windfarm +operating capacity is less than or equal to the servicing equipment's threshold.

+

TODO: This methodology needs to better resolve dispatching every equipment +relating to a request vs just the one(s) that are required. Basically, don't +dispatch every available HLV, but just one plus one of every other capability +category that has pending requests

+
+
Return type:
+

None | Generator

+
+
Parameters:
+

request (RepairRequest) --

+
+
+
+ +
+
+_run_equipment_requests(request)[source]#
+

Run the first piece of equipment (if none are onsite) for each equipment +capability category where the number of requests is greater than or equal to the +equipment's threshold.

+
+
Return type:
+

None | Generator

+
+
Parameters:
+

request (RepairRequest) --

+
+
+
+ +
+
+register_request(request)[source]#
+

The method to submit requests to the repair mananger and adds a unique +identifier for logging.

+
+
Parameters:
+

request (RepairRequest) -- The request that needs to be tracked and logged.

+
+
Return type:
+

RepairRequest

+
+
Returns:
+

RepairRequest -- The same request as passed into the method, but with a unique identifier +used for logging.

+
+
+
+ +
+
+submit_request(request)[source]#
+

The method to submit requests to the repair mananger and adds a unique +identifier for logging.

+
+
Parameters:
+

request (RepairRequest) -- The request that needs to be tracked and logged.

+
+
Return type:
+

None

+
+
Returns:
+

RepairRequest -- The same request as passed into the method, but with a unique identifier +used for logging.

+
+
+
+ +
+
+get_request_by_system(equipment_capability, system_id=None)[source]#
+

Gets all repair requests for a certain turbine with given a sequence of +equipment_capability as long as it isn't registered as unable to be +serviced.

+
+
Parameters:
+
    +
  • equipment_capability (list[str]) -- The capability of the servicing equipment requesting repairs to process.

  • +
  • system_id (Optional[str], optional) -- ID of the turbine or OSS; should correspond to System.id, by default +None. If None, then it will simply sort the list by System.id and return +the first repair requested.

  • +
+
+
Return type:
+

FilterStoreGet | None

+
+
Returns:
+

Optional[FilterStoreGet] -- The first repair request for the focal system or None, if self.items is +empty, or there is no matching system.

+
+
+
+ +
+
+get_request_by_severity(equipment_capability, severity_level=None)[source]#
+

Gets the next repair request by severity_level.

+
+
Parameters:
+
    +
  • equipment_capability (list[str]) -- The capability of the equipment requesting possible repairs to make.

  • +
  • severity_level (int) -- Severity level of focus, default None.

  • +
+
+
Return type:
+

FilterStoreGet | None

+
+
Returns:
+

Optional[FilterStoreGet] -- Repair request meeting the required criteria.

+
+
+
+ +
+
+invalidate_system(system, tow=False)[source]#
+

Disables the ability for servicing equipment to service a specific system, +sets the turbine status to be in servicing, and interrupts all the processes +to turn off operations.

+
+
Parameters:
+
    +
  • system (System | Cable | str) -- The system, cable, or str id of one to disable repairs.

  • +
  • tow (bool, optional) -- Set to True if this is for a tow-to-port request.

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+interrupt_system(system)[source]#
+

Sets the turbine status to be in servicing, and interrupts all the processes +to turn off operations.

+
+
Parameters:
+
    +
  • system_id (str) -- The system to disable repairs.

  • +
  • system (System | Cable) --

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+register_repair(repair)[source]#
+

Registers the repair as complete with the repair managiner.

+
+
Parameters:
+
    +
  • repair (RepairRequest) -- The repair that has been completed.

  • +
  • port (bool, optional) -- If True, indicates that a port handled the repair, otherwise that a managed +servicing equipment handled the repair, by default False.

  • +
+
+
Yields:
+

Generator -- The completed_requests.put() that registers completion.

+
+
Return type:
+

Generator

+
+
+
+ +
+
+enable_requests_for_system(system, tow=False)[source]#
+

Reenables service equipment operations on the provided system.

+
+
Parameters:
+
    +
  • system_id (System | Cable) -- The System or Cable that can be operated on again.

  • +
  • tow (bool, optional) -- Set to True if this is for a tow-to-port request.

  • +
  • system (System | Cable) --

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+get_all_requests_for_system(agent, system_id)[source]#
+

Gets all repair requests for a specific system_id.

+
+
Parameters:
+
    +
  • agent (str) -- The name of the entity requesting all of a system's repair requests.

  • +
  • system_id (Optional[str], optional) -- ID of the turbine or OSS; should correspond to System.id. +the first repair requested.

  • +
+
+
Return type:
+

list[RepairRequest] | None | Generator

+
+
Returns:
+

Optional[list[RepairRequest]] -- All repair requests for a given system. If no matching requests are found, +or there aren't any items in the queue yet, then None is returned.

+
+
+
+ +
+
+purge_subassembly_requests(system_id, subassembly_id, exclude=[])[source]#
+

Yields all the requests for a system/subassembly combination. This is +intended to be used to remove erroneous requests after a subassembly has been +replaced.

+
+
Parameters:
+
    +
  • system_id (str) -- Either the System.id or Cable.id.

  • +
  • subassembly_id (str) -- Either the Subassembly.id or the Cable.id repeated for cables.

  • +
  • exclude (list[str]) -- A list of request_id to exclude from the purge. This is a specific use +case for the combined cable system/subassembly, but can be to exclude +certain requests from the purge.

  • +
+
+
Yields:
+

Optional[list[RepairRequest]] -- All requests made to the repair manager for the provided system/subassembly +combination. Returns None if self.items is empty or the loop terminates +without finding what it is looking for.

+
+
Return type:
+

list[RepairRequest] | None

+
+
+
+ +
+
+property request_map: dict[str, int]#
+

Creates an updated mapping between the servicing equipment capabilities and +the number of requests that fall into each capability category (nonzero values +only).

+
+ +
+ +
+
+

Servicing Equipment#

+

The servicing equipment module provides a small number of utility functions specific +to the operations of servicing equipment and the ServiceEquipment class that provides +the repair and transportation logic for scheduled, unscheduled, and unscheduled towing +servicing equipment.

+
+
+wombat.core.service_equipment.consecutive_groups(data, step_size=1)[source]#
+

Generates the subgroups of an array where the difference between two sequential +elements is equal to the step_size. The intent is to find the length of delays +in a weather forecast.

+
+
Parameters:
+
    +
  • data (np.ndarray) -- An array of integers.

  • +
  • step_size (int, optional) -- The step size to be considered a consecutive number, by default 1.

  • +
+
+
Return type:
+

list[ndarray]

+
+
Returns:
+

list[np.ndarray] -- A list of arrays of the consecutive elements of data.

+
+
+
+ +
+
+wombat.core.service_equipment.calculate_delay_from_forecast(forecast, hours_required)[source]#
+

Calculates the delay from the binary weather forecast for if that hour is all +clear for operations.

+
+
Parameters:
+
    +
  • forecast (np.ndarray) -- Truth array to indicate if that hour satisfies the weather limit requirements.

  • +
  • hours_required (np.ndarray) -- The minimum clear weather window required, in hours.

  • +
+
+
Return type:
+

tuple[bool, int]

+
+
Returns:
+

tuple[bool, int] -- Indicator if a window is found (True) or not (False), and the number +of hours the event needs to be delayed in order to start.

+
+
+
+ +
+
+wombat.core.service_equipment.validate_end_points(start, end, no_intrasite=False)[source]#
+

Checks the starting and ending locations for traveling and towing.

+
+
Parameters:
+
    +
  • start (str) -- The starting location; should be on of: "site", "system", or "port".

  • +
  • end (str) -- The ending location; should be on of: "site", "system", or "port".

  • +
  • no_intrasite (bool) -- A flag to disable intrasite travel, so that start and end cannot +both be "system", by default False.

  • +
+
+
Raises:
+
    +
  • ValueError -- Raised if the starting location is invalid.

  • +
  • ValueError -- Raised if the ending location is invalid

  • +
  • ValueError -- Raised if "system" is provided to both start and end, but + no_intrasite is set to True.

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+wombat.core.service_equipment.reset_system_operations(system, subassembly_resets)[source]#
+

Completely resets the failure and maintenance events for a given system +and its subassemblies, and puts each Subassembly.operating_level back to 100%.

+
+

Note

+

This is only intended to be used in conjunction with a tow-to-port +repair where a turbine will be completely serviced.

+
+
+
Parameters:
+
    +
  • system (System) -- The turbine to be reset.

  • +
  • subassembly_resets (list[str]) -- The `subassembly_id`s to reset to good as new, if not assuming all +subassemblies.

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+class wombat.core.service_equipment.ServiceEquipment(env, windfarm, repair_manager, equipment_data_file)[source]#
+

Provides a servicing equipment object that can handle various maintenance and +repair tasks.

+
+
Parameters:
+
    +
  • env (WombatEnvironment) -- The simulation environment.

  • +
  • windfarm (Windfarm) -- The Windfarm object.

  • +
  • repair_manager (RepairManager) -- The RepairManager object.

  • +
  • equipment_data_file (str) -- The equipment settings file name with extension.

  • +
+
+
+
+
+env#
+

The simulation environment instance.

+
+
Type:
+

WombatEnvironment

+
+
+
+ +
+
+windfarm#
+

The simulation windfarm instance.

+
+
Type:
+

Windfarm

+
+
+
+ +
+
+manager#
+

The simulation repair manager instance.

+
+
Type:
+

RepairManager

+
+
+
+ +
+
+settings#
+

The servicing equipment's configuration settings, as provided by the user.

+
+
Type:
+

ScheduledServiceEquipmentData | UnscheduledServiceEquipmentData

+
+
+
+ +
+
+onsite#
+

Indicates if the servicing equipment is at the site (True), or not +(False).

+
+
Type:
+

bool

+
+
+
+ +
+
+enroute#
+

Indicates if the servicing equipment is on its way to the site (True), +or not (False).

+
+
Type:
+

bool

+
+
+
+ +
+
+at_port#
+

Indicates if the servicing equipment is at the port, or similar location for +land-based, (True), or not (False).

+
+
Type:
+

bool

+
+
+
+ +
+
+at_system#
+

Indications if the servicing equipment is at a cable, substation, or turbine +while on the site (True), or not (False).

+
+
Type:
+

bool

+
+
+
+ +
+
+current_system#
+

Either the System.id if at_system, or None if not.

+
+
Type:
+

str | None

+
+
+
+ +
+
+transferring_crew#
+

Indications if the servicing equipment is at a cable, substation, or turbine +and transferring the crew to or from that system (True), or not +(False).

+
+
Type:
+

bool

+
+
+
+ +
+
+finish_setup_with_environment_variables()[source]#
+

A post-initialization step that will override unset parameters with those +from the the environemt that may have already been set.

+
+
Return type:
+

None

+
+
+
+ +
+
+_register_port(port)[source]#
+

Method for a tugboat at attach the port for two-way communications. This also +sets the vessel to be at the port, and updates the port_distance.

+
+
Parameters:
+

port (Port) -- The port where the tugboat is based.

+
+
Return type:
+

None

+
+
+
+ +
+
+_set_location(end, set_current=None)[source]#
+

Keeps track of the servicing equipment by setting the location at either: +site, port, or a specific system.

+
+
Parameters:
+
    +
  • end (str) -- The ending location; one of "site", or "port"

  • +
  • set_current (str) -- The System.id for the new current location, if one is to be set.

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+_weather_forecast(hours, which)[source]#
+

Retrieves the weather forecast from the simulation environment, and +translates it to a boolean for satisfactory (True) and unsatisfactory (False) +weather conditions.

+
+
Parameters:
+
    +
  • hours (int | float) -- The number of hours of weather data that should be retrieved.

  • +
  • which (str) -- One of "repair" or "transport" to indicate which weather limits to be using.

  • +
+
+
Return type:
+

tuple[Series, Series, Series]

+
+
Returns:
+

tuple[pl.Series, pl.Series, pl.Series] -- The datetime Series, the hour of day Series, and the boolean Series of +where the weather conditions are within safe operating limits for the +servicing equipment (True) or not (False).

+
+
+
+ +
+
+get_speed(tow=False)[source]#
+

Determines the appropriate speed that the servicing equipment should be +traveling at for towing or traveling, and if the timeframe is during a reduced +speed scenario.

+
+
Parameters:
+

tow (bool, optional) -- True indicates the servicing equipment should be using the towing speed, +and if False, then the traveling speed should be used, by default False.

+
+
Return type:
+

float

+
+
Returns:
+

float -- The maximum speed the servicing equipment should be traveling/towing at.

+
+
+
+ +
+
+get_next_request()[source]#
+

Gets the next request by the rig's method for processing repairs.

+
+
Returns:
+

simpy.resources.store.FilterStoreGet -- The next RepairRequest to be processed.

+
+
+
+ +
+
+enable_string_operations(cable)[source]#
+

Traverses the upstream cable and turbine connections and resets the +System.cable_failure and Cable.downstream_failure until it hits +another cable failure, then the loop exits.

+
+
Parameters:
+
    +
  • subassembly (Cable) -- The Cable or System

  • +
  • cable (Cable) --

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+register_repair_with_subassembly(subassembly, repair, starting_operating_level)[source]#
+

Goes into the repaired subassembly, component, or cable and returns its +operating_level back to good as new for the specific repair. For fatal cable +failures, all upstream turbines are turned back on unless there is another fatal +cable failure preventing any more from operating.

+
+
Parameters:
+
    +
  • subassembly (Subassembly | Cable) -- The subassembly or cable that was repaired.

  • +
  • repair (RepairRequest) -- The request for repair that was submitted.

  • +
  • starting_operating_level (float) -- The operating level before a repair was started.

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+wait_until_next_operational_period(*, less_mobilization_hours=0)[source]#
+

Delays the crew and equipment until the start of the next operational +period.

+

TODO: Need a custom error if weather doesn't align with the equipment dates.

+
+
Parameters:
+

less_mobilization_hours (int) -- The number of hours required for mobilization that will be subtracted from +the waiting period to account for mobilization, by default 0.

+
+
Yields:
+

Generator[Timeout, None, None] -- A Timeout event for the number of hours between when the function is called +and when the next operational period starts.

+
+
Return type:
+

Generator[Timeout, None, None]

+
+
+
+ +
+
+mobilize_scheduled()[source]#
+

Mobilizes the ServiceEquipment object by waiting for the next operational +period, less the mobilization time, then logging the mobiliztion cost.

+

NOTE: weather delays are not accounted for in this process.

+
+
Yields:
+

Generator[Timeout, None, None] -- A Timeout event for the number of hours between when the function is called +and when the next operational period starts.

+
+
Return type:
+

Generator[Timeout, None, None]

+
+
+
+ +
+
+mobilize()[source]#
+

Mobilizes the ServiceEquipment object.

+

NOTE: weather delays are not accounted for at this stage.

+
+
Yields:
+

Generator[Timeout, None, None] -- A Timeout event for the number of hours the ServiceEquipment requires for +mobilizing to the windfarm site.

+
+
Return type:
+

Generator[Timeout, None, None]

+
+
+
+ +
+
+find_uninterrupted_weather_window(hours_required)[source]#
+

Finds the delay required before starting on a process that won't be able to +be interrupted by a weather delay.

+

TODO: WEATHER FORECAST NEEDS TO BE DONE FOR math.floor(self.now), not the +ceiling or there will be a whole lot of rounding up errors on process times.

+
+
Parameters:
+

hours_required (int | float) -- The number of uninterrupted of hours that a process requires for completion.

+
+
Return type:
+

tuple[int | float, bool]

+
+
Returns:
+

tuple[int | float, bool] -- The number of hours in weather delays before a process can be completed, and +an indicator for if the process has to be delayed until the next shift for +a safe transfer.

+
+
+
+ +
+
+find_interrupted_weather_window(hours_required)[source]#
+

Assesses at which points in the repair window, the wind (and wave) +constraints for safe operation are met.

+

The initial search looks for a weather window of length hours_required, and +adds 24 hours to the window for the proceeding 9 days. If no satisfactory window +is found, then the calling process must make another call to this function, but +likely there is something wrong with the constraints or weather conditions if +no window is found within 10 days.

+
+
Parameters:
+

hours_required (int | float) -- The number of hours required to complete the repair.

+
+
Return type:
+

tuple[DatetimeIndex, ndarray, bool]

+
+
Returns:
+

tuple[DatetimeIndex, np.ndarray, bool] -- The pandas DatetimeIndex, and a corresponding boolean array for what points +in the time window are safe to complete a maintenance task or repair.

+
+
+
+ +
+
+weather_delay(hours, **kwargs)[source]#
+

Processes a weather delay of length hours hours. If hours = 0, then +a Timeout is still processed, but not logging is done (this is to increase +consistency and do better typing validation across the codebase).

+
+
Parameters:
+

hours (int | float) -- The lenght, in hours, of the weather delay.

+
+
Yields:
+

Generator[Event, Any, Any] -- If the delay is more than 0 hours, then a Timeout is yielded of length +hours.

+
+
Return type:
+

Generator[Event, Any, Any]

+
+
+
+ +
+
+_calculate_intra_site_time(start, end)[source]#
+

Calculates the time it takes to travel between port and site or between +systems on site.

+
+
Parameters:
+
    +
  • start (str | None) -- The starting onsite location. If None, then 0 is returned.

  • +
  • end (str | None) -- The ending onsite location. If None, then 0 is returned.

  • +
+
+
Return type:
+

tuple[float, float]

+
+
Returns:
+

tuple[float, float] -- The travel time and distance between two locations.

+
+
+
+ +
+
+_calculate_uninterrupted_travel_time(distance, tow=False)[source]#
+

Calculates the delay to the start of traveling and the amount of time it +will take to travel between two locations.

+
+
Parameters:
+
    +
  • distance (float) -- The distance to be traveled.

  • +
  • tow (bool) -- Indicates if this travel is for towing (True), or not (False), by default +False.

  • +
+
+
Return type:
+

tuple[float, float]

+
+
Returns:
+

tuple[float, float] -- The length of the delay and the length of travel time, in hours.` -1 is +returned if there no weather windows, and the process will have to be +attempted again.

+
+
+
+ +
+
+_calculate_interrupted_travel_time(distance, tow=False)[source]#
+

Calculates the travel time with speed reductions for inclement weather, but +without shift interruptions.

+
+
Parameters:
+
    +
  • distance (flaot) -- The total distance to be traveled, in km.

  • +
  • tow (bool) -- Indicates if this travel is for towing (True), or not (False), by default +False.

  • +
+
+
Return type:
+

float

+
+
Returns:
+

float -- _description_

+
+
+
+ +
+
+travel(start, end, set_current=None, hours=None, distance=None, **kwargs)[source]#
+

The process for traveling between port and site, or two systems onsite.

+

NOTE: This does not currently take the weather conditions into account.

+
+
Parameters:
+
    +
  • start (str) -- The starting location, one of "site", "port", or "system".

  • +
  • end (str) -- The starting location, one of "site", "port", or "system".

  • +
  • set_current (str, optional) -- Where to set current_system to be if traveling to site or a different +system onsite, by default None.

  • +
  • hours (float, optional) -- The number hours required for traveling between start and end. +If provided, no internal travel time will be calculated.

  • +
  • distance (float, optional) -- The distance, in km, to be traveled. Only used if hours is provided

  • +
+
+
Yields:
+

Generator[Timeout | Process, None, None] -- The timeout event for traveling.

+
+
Return type:
+

Generator[Timeout | Process, None, None]

+
+
+
+ +
+
+tow(start, end, set_current=None, **kwargs)[source]#
+

The process for towing a turbine to/from port.

+
+
Parameters:
+
    +
  • start (str) -- The starting location; one of "site" or "port".

  • +
  • end (str) -- The ending location; one of "site" or "port".

  • +
  • set_current (str | None, optional) -- The System.id if the turbine is being returned to site, by default None

  • +
+
+
Yields:
+

Generator[Timeout | Process, None, None] -- The series of SimPy events that will be processed for the actions to occur.

+
+
Return type:
+

Generator[Timeout | Process, None, None]

+
+
+
+ +
+
+crew_transfer(system, subassembly, request, to_system)[source]#
+

The process of transfering the crew from the equipment to the System +for servicing using an uninterrupted weather window to ensure safe transfer.

+
+
Parameters:
+
    +
  • system (System | Cable) -- The System where the crew needs to be transferred to.

  • +
  • subassembly (Subassembly) -- The Subassembly that is being worked on.

  • +
  • request (RepairRequest) -- The repair to be processed.

  • +
  • to_system (bool) -- True if the crew is being transferred to the system, or False if the crew +is being transferred off the system.

  • +
+
+
Return type:
+

Generator[Timeout | Process, None, None]

+
+
Returns:
+

None -- None is returned when this is run recursively to not repeat the crew +transfer process.

+
+
Yields:
+

Generator[Timeout | Process, None, None] -- Yields a timeout event for the crew transfer once an uninterrupted weather +window can be found.

+
+
+
+ +
+
+mooring_connection(system, request, which)[source]#
+

The process of either umooring a floating turbine to prepare for towing it to +port, or reconnecting it after its repairs have been completed.

+
+
Parameters:
+
    +
  • system (System) -- The System that needs unmooring/reconnecting.

  • +
  • request (RepairRequest) -- The repair to be processed.

  • +
  • which (bool) -- "unmoor" for unmooring the turbine, "reconnect" for reconnecting the +turbine.

  • +
+
+
Return type:
+

Generator[Timeout | Process, None, None]

+
+
Returns:
+

None -- None is returned when this is run recursively to not repeat the process.

+
+
Yields:
+

Generator[Timeout | Process, None, None] -- Yields a timeout event for the unmooring/reconnection once an uninterrupted +weather window can be found.

+
+
+
+ +
+
+in_situ_repair(request, time_processed=0, prior_operation_level=-1.0, initial=False)[source]#
+

Processes the repair including any weather and shift delays.

+
+
Parameters:
+
    +
  • request (RepairRequest) -- The Maintenance or Failure receiving attention.

  • +
  • time_processed (int | float, optional) -- Time that has already been processed, by default 0.

  • +
  • prior_operation_level (float, optional) -- The operating level of the System just before the repair has begun, by +default -1.0.

  • +
  • initial (bool, optional) -- True for first step in a potentially-recursive logic, otherwise False. When +True, the repair manager will turn off the system being worked on, but if +done multiple times, the simulation will error out.

  • +
+
+
Yields:
+

Generator[Timeout | Process, None, None] -- Timeouts for the repair process.

+
+
Return type:
+

Generator[Timeout | Process, None, None]

+
+
+
+ +
+
+run_scheduled_in_situ()[source]#
+

Runs the simulation of in situ repairs for scheduled servicing equipment +that have the onsite designation or don't require mobilization.

+
+
Yields:
+

Generator[Process, None, None] -- The simulation.

+
+
Return type:
+

Generator[Process, None, None]

+
+
+
+ +
+
+run_unscheduled_in_situ()[source]#
+

Runs an in situ repair simulation for unscheduled servicing equipment, or +those that have to be mobilized before performing repairs and maintenance.

+
+
Yields:
+

Generator[Process, None, None] -- The simulation

+
+
Return type:
+

Generator[Process, None, None]

+
+
+
+ +
+
+run_tow_to_port(request)[source]#
+

Runs the tow to port logic, so a turbine can be repaired at port.

+
+
Parameters:
+

request (RepairRequest) -- The request the triggered the tow-to-port strategy.

+
+
Yields:
+

Generator[Process, None, None] -- The series of events that simulate the complete towing logic.

+
+
Raises:
+

ValueError -- Raised if the equipment is not currently at port

+
+
Return type:
+

Generator[Process, None, None]

+
+
+
+ +
+
+_check_working_hours(which)#
+

Checks the working hours of the port and overrides a default (-1) to +the env settings, otherwise hours remain the same.

+
+
Parameters:
+

which (str) -- One of "env" or "port" to determine from which overarching environment +variable should be used to override unset settings.

+
+
Return type:
+

None

+
+
+
+ +
+
+_is_workshift(hour_ix)#
+

Determines which timestamps are in the servicing equipment's working hours.

+
+
Parameters:
+

hour_ix (np.ndarray | float | int) -- The hour of day component of the datetime stamp.

+
+
Return type:
+

ndarray | bool

+
+
Returns:
+

np.ndarray | bool -- A boolean array for which values in working hours (True), and which values +are outside working hours (False).

+
+
+
+ +
+
+hours_to_next_operational_date(start_search_date, exclusion_days=0)#
+

Calculates the number of hours until the first date that is not a part of +the non_operational_dates given a starting datetime and for search criteria. +Optionally, exclusion_days can be used to account for a mobilization period +so that mobilization can occur during the non operational period.

+
+
Parameters:
+
    +
  • start_search_date (datetime.datetime | datetime.date | pd.Timestamp) -- The first date to be considered in the search.

  • +
  • exclusion_days (int, optional) -- The number of days to subtract from the next available datetime that +represents a non operational action that can occur during the non +operational period, such as mobilization, by default 0.

  • +
+
+
Return type:
+

float

+
+
Returns:
+

float -- The total number of hours until the next operational date.

+
+
+
+ +
+
+initialize_cost_calculators(which)#
+

Creates the cost calculators for each of the subclasses that will need to +calculate hourly costs.

+
+
Parameters:
+

which (str) -- One of "port" or "equipment" to to indicate how to access equipment costs

+
+
Return type:
+

None

+
+
+
+ +
+
+process_repair(hours, request_details, **kwargs)#
+

The logging and timeout process for performing a repair or doing maintenance.

+
+
Parameters:
+
    +
  • hours (int | float) -- The lenght, in hours, of the repair or maintenance task.

  • +
  • request_details (Maintenance | Failure) -- The deatils of the request, this is only being checked for the type.

  • +
  • kwargs (dict) -- Additional parameters to be passed to WombatEnvironment.log_action.

  • +
+
+
Yields:
+

Generator[Timeout | Process, None, None] -- A Timeout is yielded of length hours.

+
+
Return type:
+

Generator[Timeout | Process, None, None]

+
+
+
+ +
+
+run_tow_to_site(request, subassembly_resets=[])[source]#
+

Runs the tow to site logic for after a turbine has had its repairs completed +at port.

+
+
Parameters:
+
    +
  • request (RepairRequest) -- The request the triggered the tow-to-port strategy.

  • +
  • subassembly_resets (list[str]) -- The `subassembly_id`s to reset to good as new. Defaults to [].

  • +
+
+
Yields:
+

Generator[Process, None, None] -- The series of events that simulate the complete towing logic.

+
+
Raises:
+

ValueError -- Raised if the equipment is not currently at port

+
+
Return type:
+

Generator[Process, None, None]

+
+
+
+ +
+
+wait_until_next_shift(**kwargs)#
+

Delays the process until the start of the next shift.

+
+
Yields:
+

Generator[Timeout, None, None] -- Delay until the start of the next shift.

+
+
Return type:
+

Generator[Timeout, None, None]

+
+
+
+ +
+ +
+
+

Port#

+

Creates the Port class that provies the tow-to-port repair capabilities for +offshore floating wind farms. The Port will control a series of tugboats enabled +through the "TOW" capability that get automatically dispatched once a tow-to-port repair +is submitted and a tugboat is available (ServiceEquipment.at_port). The Port also +controls any mooring repairs through the "AHV" capability, which operates similarly to +the tow-to-port except that it will not be released until the repair is completed, and +operates on a strict shift scheduling basis.

+
+
+class wombat.core.port.Port(env, windfarm, repair_manager, config)[source]#
+

The offshore wind base port that operates tugboats and performs tow-to-port +repairs.

+
+

Note

+

The operating costs for the port are incorporated into the FixedCosts +functionality in the high-levl cost bucket: +operations_management_administration or the more granula cost bucket: +marine_management

+
+
+
Parameters:
+
    +
  • env (WombatEnvironment) -- The simulation environment instance.

  • +
  • windfarm (Windfarm) -- The simulation windfarm instance.

  • +
  • repair_manager (RepairManager) -- The simulation repair manager instance.

  • +
  • config (dict | str | Path) -- A path to a YAML object or dictionary encoding the port's configuration +settings. This will be loaded into a PortConfig object during +initialization.

  • +
+
+
+
+
+env#
+

The simulation environment instance.

+
+
Type:
+

WombatEnvironment

+
+
+
+ +
+
+windfarm#
+

The simulation windfarm instance.

+
+
Type:
+

Windfarm

+
+
+
+ +
+
+manager#
+

The simulation repair manager instance.

+
+
Type:
+

RepairManager

+
+
+
+ +
+
+settings#
+

The port's configuration settings, as provided by the user.

+
+
Type:
+

PortConfig

+
+
+
+ +
+
+requests_serviced#
+

The set of requests that have already been serviced to ensure there are no +duplications of labor when splitting out the repair requests to be processed.

+
+
Type:
+

set[str]

+
+
+
+ +
+
+turbine_manager#
+

A SimPy Resource object that limits the number of turbines that can be towed +to port, so as not to overload the quayside waters, which is controlled by +settings.max_operations.

+
+
Type:
+

simpy.Resource

+
+
+
+ +
+
+crew_manager#
+

A SimPy Resource object that limts the number of repairs that can be +occurring at any given time, which is controlled by settings.n_crews.

+
+
Type:
+

simpy.Resource

+
+
+
+ +
+
+service_equipment_manager#
+

A SimPy FilterStore object that acts as a coordination system for the +registered tugboats to tow turbines between port and site. In order to tow +in either direction they must be filtered by ServiceEquipment.at_port. This +is generated from the tugboat definitions in settings.tugboats.

+
+
Type:
+

simpy.FilterStore

+
+
+
+ +
+
+active_repairs#
+

A nested dictionary of turbines, and its associated request IDs with a SimPy +Event. The use of events allows them to automatically succeed at the end of +repairs, and once all repairs are processed on a turbine, the tow-to-site +process can commence.

+
+
Type:
+

dict[str, dict[str, simpy.events.Event]]

+
+
+
+ +
+
+_log_annual_fee()[source]#
+

Logs the annual port lease fee on a monthly-basis.

+
+ +
+
+repair_single(request)[source]#
+

Simulation logic to process a single repair request.

+
+
Parameters:
+

request (RepairRequest) -- The submitted repair or maintenance request.

+
+
Return type:
+

Generator[Timeout | Process, None, None]

+
+
+
+ +
+
+transfer_requests_from_manager(system_id)[source]#
+

Gets all of a given system's repair requests from the simulation's repair +manager, removes them from that queue, and puts them in the port's queue.

+
+
Parameters:
+

system_id (str) -- The System.id attribute from the system that will be repaired at port.

+
+
Return type:
+

None | list[RepairRequest] | Generator

+
+
Returns:
+

None | list[RepairRequest] -- The list of repair requests that need to be completed at port.

+
+
+
+ +
+
+run_repairs(system_id)[source]#
+

Method that transfers the requests from the repair manager and initiates the +repair sequence.

+
+
Parameters:
+

system_id (str) -- The System.id that is has been towed to port.

+
+
Return type:
+

Generator | None

+
+
+
+ +
+
+get_all_requests_for_system(system_id)[source]#
+

Gets all repair requests for a specific system_id.

+
+
Parameters:
+

system_id (Optional[str], optional) -- ID of the turbine or OSS; should correspond to System.id. +the first repair requested.

+
+
Return type:
+

None | Generator[FilterStoreGet, None, None]

+
+
Returns:
+

Optional[Generator[FilterStoreGet]] -- All repair requests for a given system. If no matching requests are found, +or there aren't any items in the queue yet, then None is returned.

+
+
+
+ +
+
+run_tow_to_port(request)[source]#
+

The method to initiate a tow-to-port repair sequence.

+

The process follows the following following routine:

+
    +
  1. Request a tugboat from the tugboat resource manager and wait

  2. +
  3. +
    Runs ServiceEquipment.tow_to_port, which encapsulates the traveling to

    site, unmooring, and return tow with a turbine

    +
    +
    +
  4. +
  5. Transfers the the turbine's repair log to the port, and gets all available +crews to work on repairs immediately

  6. +
  7. Requests a tugboat to return the turbine to site

  8. +
  9. Runs ServiceEquipment.tow_to_site(), which encapsulates the tow back to +site, reconnection, resetting the operating status, and returning back to +port

  10. +
+
+
Parameters:
+

request (RepairRequest) -- The request that initiated the process. This is primarily used for logging +purposes.

+
+
Yields:
+

Generator[Process, None, None] -- The series of events constituting the tow-to-port repairs

+
+
Return type:
+

Generator[Process, None, None]

+
+
+
+ +
+
+run_unscheduled_in_situ(request, initial=False)[source]#
+

Runs the in-situ repair processes for port-based servicing equipment such as +tugboats that will always return back to port, but are not necessarily a feature +of the windfarm itself, such as a crew transfer vessel.

+
+
Parameters:
+
    +
  • request (RepairRequest) -- The request that triggered the non tow-to-port, but port-based servicing +equipment repair.

  • +
  • initial (bool) --

  • +
+
+
Yields:
+

Generator[Process, None, None] -- The travel and repair processes.

+
+
Return type:
+

Generator[Process, None, None]

+
+
+
+ +
+
+_check_working_hours(which)#
+

Checks the working hours of the port and overrides a default (-1) to +the env settings, otherwise hours remain the same.

+
+
Parameters:
+

which (str) -- One of "env" or "port" to determine from which overarching environment +variable should be used to override unset settings.

+
+
Return type:
+

None

+
+
+
+ +
+
+_do_get(event)#
+

Perform the get operation.

+

This method needs to be implemented by subclasses. If the conditions +for the get event are met, the method must trigger the event (e.g. +call Event.succeed() with an appropriate value).

+

This method is called by _trigger_get() for every event in the +get_queue, as long as the return value does not evaluate +False.

+
+
Return type:
+

Optional[bool]

+
+
Parameters:
+

event (FilterStoreGet) --

+
+
+
+ +
+
+_do_put(event)#
+

Perform the put operation.

+

This method needs to be implemented by subclasses. If the conditions +for the put event are met, the method must trigger the event (e.g. +call Event.succeed() with an appropriate value).

+

This method is called by _trigger_put() for every event in the +put_queue, as long as the return value does not evaluate +False.

+
+
Return type:
+

Optional[bool]

+
+
Parameters:
+

event (StorePut) --

+
+
+
+ +
+
+_is_protocol = False#
+
+ +
+
+_is_workshift(hour_ix)#
+

Determines which timestamps are in the servicing equipment's working hours.

+
+
Parameters:
+

hour_ix (np.ndarray | float | int) -- The hour of day component of the datetime stamp.

+
+
Return type:
+

ndarray | bool

+
+
Returns:
+

np.ndarray | bool -- A boolean array for which values in working hours (True), and which values +are outside working hours (False).

+
+
+
+ +
+
+_trigger_get(put_event)#
+

Trigger get events.

+

This method is called once a new get event has been created or a put +event has been processed.

+

The method iterates over all get events in the get_queue and +calls _do_get() to check if the conditions for the event are met. +If _do_get() returns False, the iteration is stopped early.

+
+
Return type:
+

None

+
+
Parameters:
+

put_event (PutType | None) --

+
+
+
+ +
+
+_trigger_put(get_event)#
+

This method is called once a new put event has been created or a get +event has been processed.

+

The method iterates over all put events in the put_queue and +calls _do_put() to check if the conditions for the event are met. +If _do_put() returns False, the iteration is stopped early.

+
+
Return type:
+

None

+
+
Parameters:
+

get_event (GetType | None) --

+
+
+
+ +
+
+hours_to_next_operational_date(start_search_date, exclusion_days=0)#
+

Calculates the number of hours until the first date that is not a part of +the non_operational_dates given a starting datetime and for search criteria. +Optionally, exclusion_days can be used to account for a mobilization period +so that mobilization can occur during the non operational period.

+
+
Parameters:
+
    +
  • start_search_date (datetime.datetime | datetime.date | pd.Timestamp) -- The first date to be considered in the search.

  • +
  • exclusion_days (int, optional) -- The number of days to subtract from the next available datetime that +represents a non operational action that can occur during the non +operational period, such as mobilization, by default 0.

  • +
+
+
Return type:
+

float

+
+
Returns:
+

float -- The total number of hours until the next operational date.

+
+
+
+ +
+
+initialize_cost_calculators(which)#
+

Creates the cost calculators for each of the subclasses that will need to +calculate hourly costs.

+
+
Parameters:
+

which (str) -- One of "port" or "equipment" to to indicate how to access equipment costs

+
+
Return type:
+

None

+
+
+
+ +
+
+process_repair(hours, request_details, **kwargs)#
+

The logging and timeout process for performing a repair or doing maintenance.

+
+
Parameters:
+
    +
  • hours (int | float) -- The lenght, in hours, of the repair or maintenance task.

  • +
  • request_details (Maintenance | Failure) -- The deatils of the request, this is only being checked for the type.

  • +
  • kwargs (dict) -- Additional parameters to be passed to WombatEnvironment.log_action.

  • +
+
+
Yields:
+

Generator[Timeout | Process, None, None] -- A Timeout is yielded of length hours.

+
+
Return type:
+

Generator[Timeout | Process, None, None]

+
+
+
+ +
+
+wait_until_next_shift(**kwargs)#
+

Delays the process until the start of the next shift.

+
+
Yields:
+

Generator[Timeout, None, None] -- Delay until the start of the next shift.

+
+
Return type:
+

Generator[Timeout, None, None]

+
+
+
+ +
+ +
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/API/index.html b/API/index.html new file mode 100644 index 00000000..6818dee1 --- /dev/null +++ b/API/index.html @@ -0,0 +1,539 @@ + + + + + + + + + + + WOMBAT API — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

WOMBAT API

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

WOMBAT API#

+

The WOMBAT framework relies on a set of base data classes powered by the attrs +library and a series of simulation classes and methods to perform all the operations.

+

To make it easier for users, there is also a simulation interface provided.

+
+

Package Hierarchy#

+
+
+

Class Hierarchy#

+ +
+
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/API/simulation_api.html b/API/simulation_api.html new file mode 100644 index 00000000..ce49f778 --- /dev/null +++ b/API/simulation_api.html @@ -0,0 +1,1575 @@ + + + + + + + + + + + Simulation API — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Simulation API#

+ +
+

Configuration#

+
+
+class wombat.core.simulation_api.Configuration(name, layout, service_equipment, weather, workday_start, workday_end, inflation_rate, project_capacity, fixed_costs=None, port=None, start_year=None, end_year=None, port_distance=None, non_operational_start=None, non_operational_end=None, reduced_speed_start=None, reduced_speed_end=None, reduced_speed=0.0, random_seed=None, random_generator=None)[source]#
+

The Simulation configuration data class that provides all the necessary +definitions.

+
+
Parameters:
+
    +
  • name (str) -- Name of the simulation. Used for logging files.

  • +
  • layout (str) -- The windfarm layout file. See wombat.Windfarm for more details.

  • +
  • service_equipment (str | list[str]) -- The equpiment that will be used in the simulation. See +wombat.core.ServiceEquipment for more details.

  • +
  • weather (str) -- The weather profile to be used. See wombat.simulation.WombatEnvironment +for more details.

  • +
  • workday_start (int) -- Starting hour for a typical work shift. Can be overridden by +equipment-specific settings.

  • +
  • workday_end (int) -- Ending hour for a typical work shift. Can be overridden by +equipment-specific settings.

  • +
  • inflation_rate (float) -- The annual inflation rate to be used for post-processing.

  • +
  • fixed_costs (str) -- The file name for the fixed costs assumptions.

  • +
  • project_capacity (int | float) -- The total capacity of the wind plant, in MW.

  • +
  • port (dict | str | Path) -- The port configuration file or dictionary that will be used to setup a +tow-to-port repair strategy, default None.

  • +
  • port_distance (int | float) -- The simulation-wide daily travel distance for servicing equipment. This should +be used as a base setting when multiple or all servicing equipment will be +operating out of the same base location, but can be individually modified.

  • +
  • start_year (int) -- Start year of the simulation. The exact date will be determined by +the first valid date of this year in weather.

  • +
  • end_year (int) -- Final year of the simulation. The exact date will be determined by +the last valid date of this year in weather.

  • +
  • non_operational_start (str | datetime.datetime | None) -- The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of prohibited operations. When defined at the environment level, an +undefined or later starting date will be overridden for all servicing equipment +and any modeled port, by default None.

  • +
  • non_operational_end (str | datetime.datetime | None) -- The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of prohibited operations. When defined at the environment level, an +undefined or earlier ending date will be overridden for all servicing equipment +and any modeled port, by default None.

  • +
  • reduced_speed_start (str | datetime.datetime | None) -- The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of reduced speed operations. When defined at the environment level, an +undefined or later starting date will be overridden for all servicing equipment +and any modeled port, by default None.

  • +
  • reduced_speed_end (str | datetime.datetime | None) -- The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of reduced speed operations. When defined at the environment level, an +undefined or earlier ending date will be overridden for all servicing equipment +and any modeled port, by default None.

  • +
  • reduced_speed (float) -- The maximum operating speed during the annualized reduced speed operations. +When defined at the environment level, an undefined or faster value will be +overridden for all servicing equipment and any modeled port, by default 0.0.

  • +
  • random_seed (int | None) -- The random seed to be passed to a universal NumPy default_rng object to +generate Weibull random generators, by default None.

  • +
  • random_generator (np.random._generator.Generator | None) -- An optional numpy random generator that can be provided to seed a simulation +with the same generator each time, in place of the random seed. If a +random_seed is also provided, this will override the random seed, +by default None.

  • +
+
+
+
+ +
+
+

Simulation Interface#

+
+
+class wombat.core.simulation_api.Simulation(library_path, config, random_seed=None, random_generator=None)[source]#
+

The primary API to interact with the simulation methodologies.

+
+
Parameters:
+
    +
  • library_path (str) -- The path to the main data library.

  • +
  • config (Configuration | dict | str) --

    +
    One of the following:
      +
    • A pre-loaded Configuration object

    • +
    • A dictionary ready to be converted to a Configuration object

    • +
    • The name of the configuration file to be loaded, that will be located at: +library_path / config / config

    • +
    +
    +
    +

  • +
  • random_seed (int | None) -- The random seed to be passed to a universal NumPy default_rng object to +generate Weibull random generators, by default None.

  • +
  • random_generator (np.random._generator.Generator | None) -- An optional numpy random generator that can be provided to seed a simulation +with the same generator each time, in place of the random seed. If a +random_seed is also provided, this will override the random seed, +by default None.

  • +
+
+
+
+
+_create_configuration(attribute, value)[source]#
+

Validates the configuration object and creates the Configuration object +for the simulation.

+
+
Raises:
+
    +
  • TypeError -- Raised if the value provided is not able to create a valid Configuration + object

  • +
  • ValueError -- Raised if name and config.name or library_path and + config.library are not aligned.

  • +
+
+
Return type:
+

None

+
+
Returns:
+

Configuration -- The validated simulation configuration

+
+
Parameters:
+
    +
  • attribute (Attribute) --

  • +
  • value (str | Path | dict | Configuration) --

  • +
+
+
+
+ +
+
+classmethod from_config(library_path, config)[source]#
+

Creates the Simulation object only the configuration contents as either a +full file path to the configuration file, a dictionary of the configuration +contents, or pre-loaded Configuration object.

+
+
Parameters:
+
    +
  • library_path (str | Path) -- The simulation's data library. If a filename is provided for +config, this is the data library from where it will be imported. +This will also be used to feed into the returned Simulation.library_path.

  • +
  • config (str | Path | dict | Configuration) --

    The simulation configuration, see Configuration for more details on the +contents. The following is a description of the acceptable contents:

    +
      +
    • str : the full file path of the configuration yaml file.

    • +
    • dict : a dictionary with the requried configuration settings.

    • +
    • Configuration : a pre-created Configuration object.

    • +
    +

  • +
+
+
Raises:
+

TypeError -- Raised if config is not one of the three acceptable input types.

+
+
Returns:
+

Simulation -- A ready-to-run Simulation object.

+
+
+
+ +
+
+_setup_simulation()[source]#
+

Initializes the simulation objects.

+
+ +
+
+run(until=None, create_metrics=True, save_metrics_inputs=True)[source]#
+

Calls WombatEnvironment.run() and gathers the results for +post-processing. See wombat.simulation.WombatEnvironment.run or +simpy.Environment.run for more details.

+
+
Parameters:
+
    +
  • until (Optional[int | float | Event], optional) -- When to stop the simulation, by default None. See documentation on +simpy.Environment.run for more details.

  • +
  • create_metrics (bool, optional) -- If True, the metrics object will be created, and not, if False, by default +True.

  • +
  • save_metrics_inputs (bool, optional) -- If True, the metrics inputs data will be saved to a yaml file, with file +references to any larger data structures that can be reloaded later. If +False, the data will not be saved, by default True.

  • +
+
+
+
+ +
+
+save_metrics_inputs()[source]#
+

Saves the inputs for the Metrics initialization with either a direct +copy of the data or a file reference that can be loaded later.

+
+
Return type:
+

None

+
+
+
+ +
+ +
+
+

Metrics Computation#

+

For example usage of the Metrics class and its associated methods, please see the +examples documentation page

+
+
+class wombat.core.post_processor.Metrics(data_dir, events, operations, potential, production, inflation_rate, project_capacity, turbine_capacities, substation_id, turbine_id, substation_turbine_map, service_equipment_names, fixed_costs=None)[source]#
+

The metric computation class for storing logs and compiling results.

+
+
Parameters:
+
    +
  • data_dir (str | Path) --

  • +
  • events (str | pd.DataFrame) --

  • +
  • operations (str | pd.DataFrame) --

  • +
  • potential (str | pd.DataFrame) --

  • +
  • production (str | pd.DataFrame) --

  • +
  • inflation_rate (float) --

  • +
  • project_capacity (float) --

  • +
  • turbine_capacities (list[float]) --

  • +
  • substation_id (str | list[str]) --

  • +
  • turbine_id (str | list[str]) --

  • +
  • substation_turbine_map (dict[str, dict[str, list[str]]]) --

  • +
  • service_equipment_names (str | list[str]) --

  • +
  • fixed_costs (str | None) --

  • +
+
+
+
+
+_hourly_cost = 'hourly_labor_cost'#
+
+ +
+
+_salary_cost = 'salary_labor_cost'#
+
+ +
+
+_labor_cost = 'total_labor_cost'#
+
+ +
+
+_equipment_cost = 'equipment_cost'#
+
+ +
+
+_materials_cost = 'materials_cost'#
+
+ +
+
+_total_cost = 'total_cost'#
+
+ +
+
+_cost_columns = ['hourly_labor_cost', 'salary_labor_cost', 'total_labor_cost', 'equipment_cost', 'materials_cost', 'total_cost']#
+
+ +
+
+classmethod from_simulation_outputs(fpath, fname)[source]#
+

Creates the Metrics class from the saved outputs of a simulation for ease of +revisiting the calculated metrics.

+
+
Parameters:
+
    +
  • fpath (Path | str) -- The full path to the file where the data was saved.

  • +
  • fname (Path | str) -- The filename for where the data was saved, which should be a direct +dictionary mapping for the Metrics initialization.

  • +
+
+
Return type:
+

Metrics

+
+
Returns:
+

Metrics -- The class object.

+
+
+
+ +
+
+_tidy_data(data)[source]#
+

Tidies the "raw" csv-converted data to be able to be used among the +Metrics class.

+
+
Parameters:
+

data (pd.DataFrame) -- The csv log data.

+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- A tidied data frame to be used for all the operations in this class.

+
+
+
+ +
+
+_read_data(fname)[source]#
+

Reads the csv log data from library. This is intended to be used for the +events or operations data.

+
+
Parameters:
+
    +
  • path (str) -- Path to the simulation library.

  • +
  • fname (str) -- Filename of the csv data.

  • +
+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- Dataframe of either the events or operations data.

+
+
+
+ +
+
+_apply_inflation_rate(events)[source]#
+

Adjusts the cost data for compounding inflation.

+
+
Parameters:
+
    +
  • inflation_rate (float) -- The inflation rate to be applied for each year.

  • +
  • events (pd.DataFrame) -- The events dataframe containing the project cost data.

  • +
+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- The events dataframe with costs adjusted for inflation.

+
+
+
+ +
+
+time_based_availability(frequency, by)[source]#
+

Calculates the time-based availabiliy over a project's lifetime as a single +value, annual average, or monthly average for the whole windfarm or by turbine.

+
+

Note

+

This currently assumes that if there are multiple substations, that +the turbines are all connected to multiple.

+
+
+
Parameters:
+
    +
  • frequency (str) -- One of "project", "annual", "monthly", or "month-year".

  • +
  • by (str) -- One of "windfarm" or "turbine".

  • +
+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- The time-based availability at the desired aggregation level.

+
+
+
+ +
+
+production_based_availability(frequency, by)[source]#
+

Calculates the production-based availabiliy over a project's lifetime as a +single value, annual average, or monthly average for the whole windfarm or by +turbine.

+
+

Note

+

This currently assumes that if there are multiple substations, that +the turbines are all connected to multiple.

+
+
+
Parameters:
+
    +
  • frequency (str) -- One of "project", "annual", "monthly", or "month-year".

  • +
  • by (str) -- One of "windfarm" or "turbine".

  • +
+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- The production-based availability at the desired aggregation level.

+
+
+
+ +
+
+capacity_factor(which, frequency, by)[source]#
+

Calculates the capacity factor over a project's lifetime as a single value, +annual average, or monthly average for the whole windfarm or by turbine.

+
+

Note

+

This currently assumes that if there are multiple substations, that +the turbines are all connected to multiple.

+
+
+
Parameters:
+
    +
  • which (str) -- One of "net" or "gross".

  • +
  • frequency (str) -- One of "project", "annual", "monthly", or "month-year".

  • +
  • by (str) -- One of "windfarm" or "turbine".

  • +
+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- The capacity factor at the desired aggregation level.

+
+
+
+ +
+
+task_completion_rate(which, frequency)[source]#
+

Calculates the task completion rate (including tasks that are canceled after +a replacement event) over a project's lifetime as a single value, annual +average, or monthly average for the whole windfarm or by turbine.

+
+
Parameters:
+
    +
  • which (str) -- One of "scheduled", "unscheduled", or "both".

  • +
  • frequency (str) -- One of "project", "annual", "monthly", or "month-year".

  • +
+
+
Return type:
+

float | DataFrame

+
+
Returns:
+

float | pd.DataFrame -- The task completion rate at the desired aggregation level.

+
+
+
+ +
+
+equipment_costs(frequency, by_equipment=False)[source]#
+

Calculates the equipment costs for the simulation at a project, annual, or +monthly level with (or without) respect to equipment utilized in the simulation. +This excludes any port fees that might apply, which are included in: +port_fees.

+
+
Parameters:
+
    +
  • frequency (str) -- One of "project", "annual", "monthly", or "month-year".

  • +
  • by_equipment (bool, optional) -- Indicates whether the values are with resepect to the equipment utilized +(True) or not (False), by default False.

  • +
+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame --

+
+
Returns pandas DataFrame with columns:
    +
  • year (if appropriate for frequency)

  • +
  • month (if appropriate for frequency)

  • +
  • then any equipment names as they appear in the logs

  • +
+
+
+

+
+
Raises:
+
    +
  • ValueError -- If frequency is not one of "project", "annual", "monthly", or + "month-year".

  • +
  • ValueError -- If by_equipment is not one of True or False.

  • +
+
+
+
+ +
+
+service_equipment_utilization(frequency)[source]#
+

Calculates the utilization rate for each of the service equipment in the +simulation as the ratio of total number of days each of the servicing +equipment is in operation over the total number of days it's present in the +simulation. This number excludes mobilization time and the time between +visits for scheduled servicing equipment strategies.

+
+

Note

+

For tugboats in a tow-to-port scenario, this ratio will be near +100% because they are considered to be operating on an as-needed basis per +the port contracting assumptions

+
+
+
Parameters:
+

frequency (str) -- One of "project" or "annual".

+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- The utilization rate of each of the simulation SerivceEquipment.

+
+
Raises:
+

ValueError -- If frequency is not one of "project" or "annual".

+
+
+
+ +
+
+vessel_crew_hours_at_sea(frequency, by_equipment=False, vessel_crew_assumption={})[source]#
+

Calculates the total number of crew hours at sea that occurred during a +simulation at a project, annual, or monthly level that can be broken out by +servicing equipment. This includes time mobilizing, delayed at sea, servicing, +towing, and traveling.

+
+

Note

+

This metric is intended to be used for offshore wind simulations.

+
+
+
Parameters:
+
    +
  • frequency (str) -- One of "project", "annual", "monthly", or "month-year".

  • +
  • by_equipment (bool, optional) -- Indicates whether the values are with resepect to each tugboat (True) or not +(False), by default False.

  • +
  • vessel_crew_assumption (dict[str, float], optional) -- Dictionary of vessel names (ServiceEquipment.settings.name) and number +of crew members aboard to trannsform the results from vessel hours at sea +to crew hours at sea.

  • +
+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- Returns a pandas DataFrame with columns:

+
    +
  • year (if appropriate for frequency)

  • +
  • month (if appropriate for frequency)

  • +
  • Total Crew Hours at Sea

  • +
  • {ServiceEquipment.settings.name} (if broken out)

  • +
+

+
+
Raises:
+
    +
  • ValueError -- If frequency is not one of "project", "annual", "monthly", or + "month-year".

  • +
  • ValueError -- If by_equipment is not one of True or False.

  • +
  • ValueError -- If vessel_crew_assumption is not a dictionary.

  • +
+
+
+
+ +
+
+number_of_tows(frequency, by_tug=False, by_direction=False)[source]#
+

Calculates the total number of tows that occurred during a simulation at a +project, annual, or monthly level that can be broken out by tugboat.

+
+
Parameters:
+
    +
  • frequency (str) -- One of "project", "annual", "monthly", or "month-year".

  • +
  • by_tug (bool, optional) -- Indicates whether the values are with resepect to each tugboat (True) or not +(False), by default False.

  • +
  • by_direction (bool, optional) -- Indicates whether the values are with respect to the direction a turbine is +towed (True) or not (False), by default False.

  • +
+
+
Return type:
+

float | DataFrame

+
+
Returns:
+

float | pd.DataFrame -- Returns either a float for whole project-level costs or a pandas +DataFrame with columns:

+
    +
  • year (if appropriate for frequency)

  • +
  • month (if appropriate for frequency)

  • +
  • total_tows

  • +
  • total_tows_to_port (if broken out)

  • +
  • total_tows_to_site (if broken out)

  • +
  • {ServiceEquipment.settings.name}_total_tows (if broken out)

  • +
  • {ServiceEquipment.settings.name}_to_port (if broken out)

  • +
  • {ServiceEquipment.settings.name}_to_site (if broken out)

  • +
+

+
+
Raises:
+
    +
  • ValueError -- If frequency is not one of "project", "annual", "monthly", or + "month-year".

  • +
  • ValueError -- If by_tug is not one of True or False.

  • +
  • ValueError -- If by_direction is not one of True or False.

  • +
+
+
+
+ +
+
+labor_costs(frequency, by_type=False)[source]#
+

Calculates the labor costs for the simulation at a project, annual, or +monthly level that can be broken out by hourly and salary labor costs.

+
+
Parameters:
+
    +
  • frequency (str) -- One of "project", "annual", "monthly", or "month-year".

  • +
  • by_type (bool, optional) -- Indicates whether the values are with resepect to the labor types +(True) or not (False), by default False.

  • +
+
+
Return type:
+

float | DataFrame

+
+
Returns:
+

float | pd.DataFrame -- Returns either a float for whole project-level costs or a pandas +DataFrame with columns:

+
    +
  • year (if appropriate for frequency)

  • +
  • month (if appropriate for frequency)

  • +
  • total_labor_cost

  • +
  • hourly_labor_cost (if broken out)

  • +
  • salary_labor_cost (if broken out)

  • +
+

+
+
Raises:
+
    +
  • ValueError -- If frequency is not one of "project", "annual", "monthly", or + "month-year".

  • +
  • ValueError -- If by_type is not one of True or False.

  • +
+
+
+
+ +
+
+equipment_labor_cost_breakdowns(frequency, by_category=False, by_equipment=False)[source]#
+

Calculates the producitivty cost and time breakdowns for the simulation at a +project, annual, or monthly level that can be broken out to include the +equipment and labor components, as well as be broken down by servicing +equipment.

+
+

Note

+

Doesn't produce a value if there's no cost associated with a "reason".

+
+
+
Parameters:
+
    +
  • frequency (str) -- One of "project", "annual", "monthly", or "month-year".

  • +
  • by_category (bool, optional) -- Indicates whether to include the equipment and labor categories (True) or +not (False), by default False.

  • +
  • by_equipment (bool, optional) -- Indicates whether the values are with resepect to the equipment utilized +(True) or not (False), by default False.

  • +
+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame --

+
+
Returns pandas DataFrame with columns:
    +
  • year (if appropriate for frequency)

  • +
  • month (if appropriate for frequency)

  • +
  • reason

  • +
  • hourly_labor_cost (if by_category == True)

  • +
  • salary_labor_cost (if by_category == True)

  • +
  • total_labor_cost (if by_category == True)

  • +
  • equipment_cost (if by_category == True)

  • +
  • total_cost (if broken out)

  • +
  • total_hours

  • +
+
+
+

+
+
Raises:
+
    +
  • ValueError -- If frequency is not one of "project", "annual", "monthly", or + "month-year".

  • +
  • ValueError -- If by_category is not one of True or False.

  • +
+
+
+
+ +
+
+emissions(emissions_factors, maneuvering_factor=0.1, port_engine_on_factor=0.25)[source]#
+

Calculates the emissions, typically in tons, per hour of operations for +transiting, maneuvering (calculated as a % of transiting), idling at the site +(repairs, crew transfer, weather delays), and idling at port (weather delays), +excluding waiting overnight between shifts.

+
+
Parameters:
+
    +
  • emissions_factors (dict) -- Dictionary of emissions per hour for "transit", "maneuver", "idle at site", +and "idle at port" for each of the servicing equipment in the simulation.

  • +
  • maneuvering_factor (float, optional) -- The proportion of transit time that can be attributed to +maneuvering/positioning, by default 0.1.

  • +
  • port_engine_on_factor (float, optional) -- The proportion of idling at port time that can be attributed to having the +engine on and producing emissions, by default 0.25.

  • +
+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- DataFrame of "duration" (hours), "distance_km", and "emissions" (tons) for +each servicing equipment in the simulation for each emissions category.

+
+
Raises:
+
    +
  • KeyError -- Raised if any of the servicing equipment are missing from the + emissions_factors dictionary.

  • +
  • KeyError -- Raised if any of the emissions categories are missing from each servcing + equipment definition in emissions_factors.

  • +
+
+
+
+ +
+
+component_costs(frequency, by_category=False, by_action=False)[source]#
+

Calculates the component costs for the simulation at a project, annual, or +monthly level that can be broken out by cost categories. This will not sum to +the total cost because it is does not include times where there is no work being +done, but costs are being accrued.

+
+

Note

+

It should be noted that the costs will include costs accrued from both +weather delays and shift-to-shift delays. In the future these will be +disentangled.

+
+
+
Parameters:
+
    +
  • frequency (str) -- One of "project", "annual", "monthly", or "month-year".

  • +
  • by_category (bool, optional) -- Indicates whether the values are with resepect to the various cost +categories (True) or not (False), by default False.

  • +
  • by_action (bool, optional) -- Indicates whether component costs are going to be further broken out by the +action being performed--repair, maintenance, and delay--(True) or not +(False), by default False.

  • +
+
+
Return type:
+

DataFrame

+
+
Returns:
+

float | pd.DataFrame -- Returns either a float for whole project-level costs or a pandas +DataFrame with columns:

+
    +
  • year (if appropriate for frequency)

  • +
  • month (if appropriate for frequency)

  • +
  • component

  • +
  • action (if broken out)

  • +
  • materials_cost (if broken out)

  • +
  • total_labor_cost (if broken out)

  • +
  • equipment_cost (if broken out)

  • +
  • total_cost

  • +
+

+
+
Raises:
+
    +
  • ValueError -- If frequency is not one of "project", "annual", "monthly", or + "month-year".

  • +
  • ValueError -- If by_category is not one of True or False.

  • +
  • ValueError -- If by_action is not one of True or False.

  • +
+
+
+
+ +
+
+port_fees(frequency)[source]#
+

Calculates the port fees for the simulation at a project, annual, or monthly +level. This excludes any equipment or labor costs, which are included in: +equipment_costs.

+
+
Parameters:
+

frequency (str) -- One of "project" or "annual", "monthly", ".

+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- The broken out by time port fees with

+
+
Raises:
+

ValueError -- If frequency not one of "project" or "annual".

+
+
+
+ +
+
+project_fixed_costs(frequency, resolution)[source]#
+

Calculates the fixed costs of a project at the project and annual frequencies +at a given cost breakdown resolution.

+
+
Parameters:
+
    +
  • frequency (str) -- One of "project" or "annual", "monthly", ".

  • +
  • resolution (st) --

    One of "low", "medium", or "high", where the values correspond to:

    +
      +
    • low: FixedCosts.resolution["low"], corresponding to itemized costs.

    • +
    • medium: FixedCosts.resolution["medium"], corresponding to the +overarching cost categories.

    • +
    • high: FixedCosts.resolution["high"], corresponding to a lump sum.

    • +
    +

    These values can also be seen through the FixedCosts.hierarchy

    +

  • +
+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- The project's fixed costs as a sum or annualized with high, medium, and low +resolution as desired.

+
+
Raises:
+
    +
  • ValueError -- If frequency not one of "project" or "annual".

  • +
  • ValueError -- If resolution must be one of "low", "medium", or "high".

  • +
+
+
+
+ +
+
+opex(frequency, by_category=False)[source]#
+

Calculates the project's OpEx for the simulation at a project, annual, or +monthly level.

+
+
Parameters:
+
    +
  • frequency (str) -- One of project, annual, monthly, or month-year.

  • +
  • by_category (bool, optional) -- Indicates whether the values are with resepect to the various cost +categories (True) or not (False), by default False.

  • +
+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- The project's OpEx broken out at the desired time and category resolution.

+
+
+
+ +
+
+process_times()[source]#
+

Calculates the time, in hours, to complete a repair/maintenance request, on +both a request to completion basis, and the actual time to complete the repair.

+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame --

+
    +
  • category (index): repair/maintenance category

  • +
  • time_to_completion: total number of hours from the time of request to the +time of completion

  • +
  • process_time: total number of hours it took for the equipment to complete

  • +
  • the request.

  • +
  • downtime: total number of hours where the operations were below 100%.

  • +
  • N: total number of processes in the category.

  • +
+

+
+
+
+ +
+
+power_production(frequency, by='windfarm', units='gwh')[source]#
+

Calculates the power production for the simulation at a project, annual, or +monthly level that can be broken out by turbine.

+
+
Parameters:
+
    +
  • frequency (str) -- One of "project", "annual", "monthly", or "month-year".

  • +
  • by (str) -- One of "windfarm" or "turbine".

  • +
  • units (str) -- One of "gwh", "mwh", or "kwh".

  • +
+
+
Return type:
+

float | DataFrame

+
+
Returns:
+

float | pd.DataFrame -- Returns either a float for whole project-level costs or a pandas +DataFrame with columns:

+
    +
  • year (if appropriate for frequency)

  • +
  • month (if appropriate for frequency)

  • +
  • total_power_production

  • +
  • <turbine_id>_power_production (if broken out)

  • +
+

+
+
Raises:
+
    +
  • ValueError -- If frequency is not one of "project", "annual", "monthly", or + "month-year".

  • +
  • ValueError -- If by_turbine is not one of True or False.

  • +
+
+
+
+ +
+
+npv(frequency, discount_rate=0.025, offtake_price=80)[source]#
+

Calculates the net present value of the windfarm at a project, annual, or +monthly resolution given a base discount rate and offtake price.

+
+

Note

+

This function will be improved over time to incorporate more of the +financial parameter at play, such as PPAs.

+
+
+
Parameters:
+
    +
  • frequency (str) -- One of "project", "annual", "monthly", or "month-year".

  • +
  • discount_rate (float, optional) -- The rate of return that could be earned on alternative investments, by +default 0.025.

  • +
  • offtake_price (float, optional) -- Price of energy, per MWh, by default 80.

  • +
+
+
Return type:
+

DataFrame

+
+
Returns:
+

pd.DataFrame -- The project net prsent value at the desired time resolution.

+
+
+
+ +
+ +
+
+ + + + +
+ + + + + + + + +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/API/types.html b/API/types.html new file mode 100644 index 00000000..6d1e6b06 --- /dev/null +++ b/API/types.html @@ -0,0 +1,1676 @@ + + + + + + + + + + + Data Classes — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

Data Classes

+ +
+
+ +
+

Contents

+
+ +
+
+
+ + + + +
+ +
+

Data Classes#

+

The WOMBAT architecture relies heavily on a base set of data classes to process most of +the model's inputs. This enables a rigid, yet robust data model to properly define a +simulation.

+ +
+

What is this FromDictMixin I keep seeing in the code diagrams?#

+

The FromDictMixin class provides a standard method for providing dictionary definitions +to attrs dataclasses without worrying about overloading the definition. Plus, you get +the convenience of writing cls.from_dict(data_dict) instead of cls(**data_dict), and +hoping for the best.

+
+
+class wombat.core.data_classes.FromDictMixin[source]#
+

A Mixin class to allow for kwargs overloading when a data class doesn't +have a specific parameter definied. This allows passing of larger dictionaries +to a data class without throwing an error.

+
+
Raises:
+

AttributeError -- Raised if the required class inputs are not provided.

+
+
+
+
+classmethod from_dict(data)[source]#
+

Map a data dictionary to an attrs-defined class.

+

TODO: Add an error to ensure that either none or all the parameters are passed

+
+
Parameters:
+

data (dict) -- The data dictionary to be mapped.

+
+
Returns:
+

+
clsAny

The attrs-defined class.

+
+
+

+
+
+
+ +
+ +
+
+

Scheduled and Unscheduled Maintenance#

+
+

Maintenance Tasks#

+
+
+class wombat.core.data_classes.Maintenance(time, materials, frequency, service_equipment, system_value, description='routine maintenance', operation_reduction=0.0, level=0)[source]#
+

Data class to store maintenance data used in subassembly and cable modeling.

+
+
Parameters:
+
    +
  • time (float) -- Amount of time required to perform maintenance, in hours.

  • +
  • materials (float) -- Cost of materials required to perform maintenance, in USD.

  • +
  • frequency (float) -- Optimal number of days between performing maintenance, in days.

  • +
  • service_equipment (list[str] | str) --

    Any combination of th following Equipment.capability options.

    +
      +
    • RMT: remote (no actual equipment BUT no special implementation)

    • +
    • DRN: drone

    • +
    • CTV: crew transfer vessel/vehicle

    • +
    • SCN: small crane (i.e., field support vessel)

    • +
    • LCN: large crane (i.e., heavy lift vessel)

    • +
    • CAB: cabling vessel/vehicle

    • +
    • DSV: diving support vessel

    • +
    • TOW: tugboat or towing equipment

    • +
    • AHV: anchor handling vessel (tugboat that doesn't trigger tow-to-port)

    • +
    +

  • +
  • system_value (Union[int, float]) -- Turbine replacement value. Used if the materials cost is a proportional cost.

  • +
  • description (str) -- A short text description to be used for logging.

  • +
  • operation_reduction (float) --

    Performance reduction caused by the failure, between (0, 1]. Defaults to 0.

    +
    +

    Warning

    +

    As of v0.7, availability is very sensitive to the usage of this +parameter, and so it should be used carefully.

    +
    +

  • +
  • level (int, optional) -- Severity level of the maintenance. Defaults to 0.

  • +
+
+
+
+
+request_id: str#
+
+ +
+
+assign_id(request_id)[source]#
+

Assign a unique identifier to the request.

+
+
Parameters:
+

request_id (str) -- The wombat.core.RepairManager generated identifier.

+
+
Return type:
+

None

+
+
+
+ +
+ +
+
+

Failures#

+
+
+class wombat.core.data_classes.Failure(scale, shape, time, materials, operation_reduction, level, service_equipment, system_value, rng, replacement=False, description='failure')[source]#
+

Data class to store failure data used in subassembly and cable modeling.

+
+
Parameters:
+
    +
  • scale (float) -- Weibull scale parameter for a failure classification.

  • +
  • shape (float) -- Weibull shape parameter for a failure classification.

  • +
  • time (float) -- Amount of time required to complete the repair, in hours.

  • +
  • materials (float) -- Cost of the materials required to complete the repair, in $USD.

  • +
  • operation_reduction (float) --

    Performance reduction caused by the failure, between (0, 1].

    +
    +

    Warning

    +

    As of v0.7, availability is very sensitive to the usage of this +parameter, and so it should be used carefully.

    +
    +

  • +
  • level (int, optional) -- Level of severity, will be generated in the ComponentData.create_severities +method.

  • +
  • service_equipment (list[str] | str) --

    Any combination of the following Equipment.capability options:

    +
      +
    • RMT: remote (no actual equipment BUT no special implementation)

    • +
    • DRN: drone

    • +
    • CTV: crew transfer vessel/vehicle

    • +
    • SCN: small crane (i.e., field support vessel)

    • +
    • LCN: large crane (i.e., heavy lift vessel)

    • +
    • CAB: cabling vessel/vehicle

    • +
    • DSV: diving support vessel

    • +
    • TOW: tugboat or towing equipment

    • +
    • AHV: anchor handling vessel (tugboat that doesn't trigger tow-to-port)

    • +
    +

  • +
  • system_value (Union[int, float]) -- Turbine replacement value. Used if the materials cost is a proportional cost.

  • +
  • replacement (bool) -- True if triggering the failure requires a subassembly replacement, False, if +only a repair is necessary. Defaults to False

  • +
  • description (str) -- A short text description to be used for logging.

  • +
  • rng (np.random._generator.Generator) --

    +

    Note

    +

    Do not provide this, it comes from +wombat.core.environment.WombatEnvironment

    +
    +

    The shared random generator used for the Weibull sampling. This is fed through +the simulation environment to ensure consistent seeding between simulations.

    +

  • +
+
+
+
+
+request_id: str#
+
+ +
+
+hours_to_next_failure()[source]#
+

Sample the next time to failure in a Weibull distribution. If the scale +and shape parameters are set to 0, then the model will return None to +cause the subassembly to timeout to the end of the simulation.

+
+
Return type:
+

float | None

+
+
Returns:
+

float | None -- Returns None for a non-modelled failure, or the time until the next +simulated failure.

+
+
+
+ +
+
+assign_id(request_id)[source]#
+

Assign a unique identifier to the request.

+
+
Parameters:
+

request_id (str) -- The wombat.core.RepairManager generated identifier.

+
+
Return type:
+

None

+
+
+
+ +
+ +
+
+

Repair Requests#

+
+
+class wombat.core.data_classes.RepairRequest(system_id, system_name, subassembly_id, subassembly_name, severity_level, details, *, cable=False, upstream_turbines=_Nothing.NOTHING, upstream_cables=_Nothing.NOTHING)[source]#
+

Repair/Maintenance request data class.

+
+
Parameters:
+
    +
  • system_id (str) -- System.id.

  • +
  • system_name (str) -- System.name.

  • +
  • subassembly_id (str) -- Subassembly.id.

  • +
  • subassembly_name (str) -- Subassembly.name.

  • +
  • severity_level (int) -- Maintenance.level or Failure.level.

  • +
  • details (Failure | Maintenance) -- The actual data class.

  • +
  • cable (bool) -- Indicator that the request is for a cable, by default False.

  • +
  • upstream_turbines (list[str]) -- The cable's upstream turbines, by default []. No need to use this if +cable == False.

  • +
  • upstream_cables (list[str]) -- The cable's upstream cables, by default []. No need to use this if +cable == False.

  • +
+
+
+
+
+request_id: str#
+
+ +
+
+assign_id(request_id)[source]#
+

Assign a unique identifier to the request.

+
+
Parameters:
+

request_id (str) -- The wombat.core.RepairManager generated identifier.

+
+
Return type:
+

None

+
+
+
+ +
+ +
+
+
+

Servicing Equipment and Crews#

+
+

Service Equipment#

+
+
+class wombat.core.data_classes.ServiceEquipmentData(data_dict, *, strategy=None)[source]#
+

Helps to determine the type ServiceEquipment that should be used, based on the +repair strategy for its operation. See +ScheduledServiceEquipmentData or +UnscheduledServiceEquipmentData for more details on each +classifcation.

+
+
Parameters:
+
    +
  • data_dict (dict) -- The dictionary that will be used to create the appropriate ServiceEquipmentData. +This should contain a field called 'strategy' with either "scheduled" or +"unscheduled" as a value if strategy is not provided as a keyword argument.

  • +
  • strategy (str, optional) -- Should be one of "scheduled", "requests", "downtime". If nothing is provided, +the equipment configuration will be checked.

  • +
+
+
Raises:
+

ValueError -- Raised if strategy is not one of "scheduled" or "unscheduled".

+
+
+

Examples

+

The below workflow is how a new data +ScheduledServiceEquipmentData object could be created via +a generic/routinized creation method, and is how the +ServiceEquipment's __init__ method creates the +settings data.

+
>>> from wombat.core.data_classes import  ServiceEquipmentData
+>>>
+>>> data_dict = {
+>>>     "name": "Crew Transfer Vessel 1",
+>>>     "equipment_rate": 1750,
+>>>     "start_month": 1,
+>>>     "start_day": 1,
+>>>     "end_month": 12,
+>>>     "end_day": 31,
+>>>     "start_year": 2002,
+>>>     "end_year": 2014,
+>>>     "onsite": True,
+>>>     "capability": "CTV",
+>>>     "max_severity": 10,
+>>>     "mobilization_cost": 0,
+>>>     "mobilization_days": 0,
+>>>     "speed": 37.04,
+>>>     "max_windspeed_transport": 99,
+>>>     "max_windspeed_repair": 99,
+>>>     "max_waveheight_transport": 1.5,
+>>>     "max_waveheight_repair": 1.5,
+>>>     "strategy": scheduled,
+>>>     "crew_transfer_time": 0.25,
+>>>     "n_crews": 1,
+>>>     "crew": {
+>>>         "day_rate": 0,
+>>>         "n_day_rate": 0,
+>>>         "hourly_rate": 0,
+>>>         "n_hourly_rate": 0,
+>>>     },
+>>> }
+>>> equipment = ServiceEquipmentData(data_dict).determine_type()
+>>> type(equipment)
+
+
+
+
+determine_type()[source]#
+

Generate the appropriate ServiceEquipmentData variation.

+
+
Return type:
+

ScheduledServiceEquipmentData | UnscheduledServiceEquipmentData

+
+
Returns:
+

Union[ScheduledServiceEquipmentData, UnscheduledServiceEquipmentData] -- The appropriate xxServiceEquipmentData schema depending on the strategy +the ServiceEquipment will use.

+
+
+
+ +
+ +
+
+

ServiceCrew#

+
+
+class wombat.core.data_classes.ServiceCrew(n_day_rate, day_rate, n_hourly_rate, hourly_rate)[source]#
+

An internal data class for the indivdual crew units that are on the servicing +equipment.

+
+
Parameters:
+
    +
  • n_day_rate (int) -- Number of salaried workers.

  • +
  • day_rate (float) -- Day rate for salaried workers, in USD.

  • +
  • n_hourly_rate (int) -- Number of hourly/subcontractor workers.

  • +
  • hourly_rate (float) -- Hourly labor rate for subcontractors, in USD.

  • +
+
+
+
+ +
+
+

Scheduled Service Equipment#

+
+
+class wombat.core.data_classes.ScheduledServiceEquipmentData(name, equipment_rate, n_crews, crew, capability, speed, max_windspeed_transport, max_windspeed_repair, mobilization_cost=0, mobilization_days=0, max_waveheight_transport=1000.0, max_waveheight_repair=1000.0, workday_start=-1, workday_end=-1, crew_transfer_time=0.0, speed_reduction_factor=0.0, port_distance=0.0, onsite=False, method='severity', start_month=-1, start_day=-1, start_year=-1, end_month=-1, end_day=-1, end_year=-1, strategy='scheduled', non_operational_start=None, non_operational_end=None, reduced_speed_start=None, reduced_speed_end=None, reduced_speed=0)[source]#
+

The data class specification for servicing equipment that will use a +pre-scheduled basis for returning to site.

+
+
Parameters:
+
    +
  • name (str) -- Name of the piece of servicing equipment.

  • +
  • equipment_rate (float) -- Day rate for the equipment/vessel, in USD.

  • +
  • n_crews (int) --

    Number of crew units for the equipment.

    +
    +

    Note

    +

    The input to this does not matter yet, as multi-crew functionality +is not yet implemented.

    +
    +

  • +
  • crew (ServiceCrew) -- The crew details, see ServiceCrew for more information. Dictionary +of labor costs with the following: n_day_rate, day_rate, +n_hourly_rate, and hourly_rate.

  • +
  • start_month (int) -- The day to start operations for the rig and crew.

  • +
  • start_day (int) -- The month to start operations for the rig and crew.

  • +
  • start_year (int) -- The year to start operations for the rig and crew.

  • +
  • end_month (int) -- The month to end operations for the rig and crew.

  • +
  • end_day (int) -- The day to end operations for the rig and crew.

  • +
  • end_year (int) --

    The year to end operations for the rig and crew.

    +
    +

    Note

    +

    if the rig comes annually, then the enter the year for the last year +that the rig and crew will be available.

    +
    +

  • +
  • capability (str) --

    The type of capabilities the equipment contains. Must be one of:

    +
      +
    • RMT: remote (no actual equipment BUT no special implementation)

    • +
    • DRN: drone

    • +
    • CTV: crew transfer vessel/vehicle

    • +
    • SCN: small crane (i.e., field support vessel)

    • +
    • LCN: large crane (i.e., heavy lift vessel)

    • +
    • CAB: cabling vessel/vehicle

    • +
    • DSV: diving support vessel

    • +
    +

    Please note that "TOW" is unavailable for scheduled servicing equipment

    +

  • +
  • mobilization_cost (float) -- Cost to mobilize the rig and crew.

  • +
  • mobilization_days (int) -- Number of days it takes to mobilize the equipment.

  • +
  • speed (float) -- Maximum transit speed, km/hr.

  • +
  • speed_reduction_factor (flaot) -- Reduction factor for traveling in inclement weather, default 0. When 0, travel +is stopped when either max_windspeed_transport or max_waveheight_transport +is reached, and when 1, speed is used.

  • +
  • max_windspeed_transport (float) -- Maximum windspeed for safe transport, m/s.

  • +
  • max_windspeed_repair (float) -- Maximum windspeed for safe operations, m/s.

  • +
  • max_waveheight_transport (float) -- Maximum waveheight for safe transport, m, default 1000 (land-based).

  • +
  • max_waveheight_repair (float) -- Maximum waveheight for safe operations, m, default 1000 (land-based).

  • +
  • workday_start (int) -- The starting hour of a workshift, in 24 hour time.

  • +
  • workday_end (int) -- The ending hour of a workshift, in 24 hour time.

  • +
  • crew_transfer_time (float) -- The number of hours it takes to transfer the crew from the equipment to the +system, e.g. how long does it take to transfer the crew from the CTV to the +turbine, default 0.

  • +
  • onsite (bool) --

    Indicator for if the servicing equipment and crew are based onsite.

    +
    +

    Note

    +

    If based onsite, be sure that the start and end dates represent the +first and last day/month of the year, respectively, and the start and end +years represent the fist and last year in the weather file.

    +
    +

  • +
  • method (str) -- Determines if the equipment will do all maximum severity repairs first or do all +the repairs at one turbine before going to the next, by default severity. Must +be one of "severity" or "turbine".

  • +
  • port_distance (int | float) -- The distance, in km, the equipment must travel to go between port and site, by +default 0.

  • +
  • non_operational_start (str | datetime.datetime | None) -- The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of prohibited operations. When defined at the environment level, an +undefined or later starting date will be overridden, by default None.

  • +
  • non_operational_end (str | datetime.datetime | None) -- The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of prohibited operations. When defined at the environment level, an +undefined or earlier ending date will be overridden, by default None.

  • +
  • reduced_speed_start (str | datetime.datetime | None) -- The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of reduced speed operations. When defined at the environment level, an +undefined or later starting date will be overridden, by default None.

  • +
  • reduced_speed_end (str | datetime.datetime | None) -- The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of reduced speed operations. When defined at the environment level, an +undefined or earlier ending date will be overridden, by default None.

  • +
  • reduced_speed (float) -- The maximum operating speed during the annualized reduced speed operations. +When defined at the environment level, an undefined or faster value will be +overridden, by default 0.0.

  • +
  • strategy (str) --

  • +
+
+
+
+
+operating_dates_set: set#
+
+ +
+
+non_operational_dates: DatetimeIndex#
+
+ +
+
+non_operational_dates_set: DatetimeIndex#
+
+ +
+
+reduced_speed_dates: DatetimeIndex#
+
+ +
+
+non_stop_shift: bool#
+
+ +
+ +
+
+

Unscheduled Service Equipment#

+
+
+class wombat.core.data_classes.UnscheduledServiceEquipmentData(name, equipment_rate, n_crews, crew, capability, speed, max_windspeed_transport, max_windspeed_repair, mobilization_cost=0, mobilization_days=0, max_waveheight_transport=1000.0, max_waveheight_repair=1000.0, workday_start=-1, workday_end=-1, crew_transfer_time=0.0, speed_reduction_factor=0.0, port_distance=0.0, onsite=False, method='severity', strategy='unscheduled', strategy_threshold=-1, charter_days=-1, tow_speed=1, unmoor_hours=0, reconnection_hours=0, non_operational_start=None, non_operational_end=None, reduced_speed_start=None, reduced_speed_end=None, reduced_speed=0)[source]#
+

The data class specification for servicing equipment that will use either a +basis of windfarm downtime or total number of requests serviceable by the equipment.

+
+
Parameters:
+
    +
  • name (str) -- Name of the piece of servicing equipment.

  • +
  • equipment_rate (float) -- Day rate for the equipment/vessel, in USD.

  • +
  • n_crews (int) --

    Number of crew units for the equipment.

    +

  • +
  • crew (ServiceCrew) -- The crew details, see ServiceCrew for more information. Dictionary +of labor costs with the following: n_day_rate, day_rate, +n_hourly_rate, and hourly_rate.

  • +
  • charter_days (int) -- The number of days the servicing equipment can be chartered for.

  • +
  • capability (str) -- The type of capabilities the equipment contains. Must be one of: +- RMT: remote (no actual equipment BUT no special implementation) +- DRN: drone +- CTV: crew transfer vessel/vehicle +- SCN: small crane (i.e., field support vessel) +- LCN: large crane (i.e., heavy lift vessel) +- CAB: cabling vessel/vehicle +- DSV: diving support vessel +- TOW: tugboat or towing equipment +- AHV: anchor handling vessel (tugboat that doesn't trigger tow-to-port)

  • +
  • speed (float) -- Maximum transit speed, km/hr.

  • +
  • tow_speed (float) --

    The maximum transit speed when towing, km/hr.

    +
    +

    Note

    +

    This is only required for when the servicing equipment is tugboat +enabled for a tow-to-port scenario (capability = "TOW")

    +
    +

  • +
  • speed_reduction_factor (flaot) -- Reduction factor for traveling in inclement weather, default 0. When 0, travel +is stopped when either max_windspeed_transport or max_waveheight_transport +is reached, and when 1, speed is used.

  • +
  • max_windspeed_transport (float) -- Maximum windspeed for safe transport, m/s.

  • +
  • max_windspeed_repair (float) -- Maximum windspeed for safe operations, m/s.

  • +
  • max_waveheight_transport (float) -- Maximum waveheight for safe transport, m, default 1000 (land-based).

  • +
  • max_waveheight_repair (float) -- Maximum waveheight for safe operations, m, default 1000 (land-based).

  • +
  • mobilization_cost (float) -- Cost to mobilize the rig and crew, default 0.

  • +
  • mobilization_days (int) -- Number of days it takes to mobilize the equipment, default 0.

  • +
  • strategy (str) -- For any unscheduled maintenance servicing equipment, this determines the +strategy for dispatching. Should be on of "downtime" or "requests".

  • +
  • strategy_threshold (str) -- For downtime-based scenarios, this is based on the operating level, and should +be in the range (0, 1). For reqest-based scenarios, this is the maximum number +of requests that are allowed to build up for any given type of unscheduled +servicing equipment, should be an integer >= 1.

  • +
  • workday_start (int) -- The starting hour of a workshift, in 24 hour time.

  • +
  • workday_end (int) -- The ending hour of a workshift, in 24 hour time.

  • +
  • crew_transfer_time (float) -- The number of hours it takes to transfer the crew from the equipment to the +system, e.g. how long does it take to transfer the crew from the CTV to the +turbine, default 0.

  • +
  • onsite (bool) --

    Indicator for if the rig and crew are based onsite.

    +
    +

    Note

    +

    if the rig and crew are onsite be sure that the start and end dates +represent the first and last day/month of the year, respectively, and the +start and end years represent the fist and last year in the weather file.

    +
    +

  • +
  • method (str) -- Determines if the ship will do all maximum severity repairs first or do all +the repairs at one turbine before going to the next, by default severity. +Should by one of "severity" or "turbine".

  • +
  • unmoor_hours (int | float) --

    The number of hours required to unmoor a floating offshore wind turbine in order +to tow it to port, by default 0.

    +
    +

    Note

    +

    Required for the tugboat/towing capability, otherwise unused.

    +
    +

  • +
  • reconnection_hours (int | float) --

    The number of hours required to reconnect a floating offshore wind turbine after +being towed back to site, by default 0.

    +
    +

    Note

    +

    Required for the tugboat/towing capability, otherwise unused.

    +
    +

  • +
  • port_distance (int | float) -- The distance, in km, the equipment must travel to go between port and site, by +default 0.

  • +
  • non_operational_start (str | datetime.datetime | None) -- The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of prohibited operations. When defined at the environment level or the +port level, if a tugboat, an undefined or later starting date will be +overridden, by default None.

  • +
  • non_operational_end (str | datetime.datetime | None) -- The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of prohibited operations. When defined at the environment level or the +port level, if a tugboat, an undefined or earlier ending date will be +overridden, by default None.

  • +
  • reduced_speed_start (str | datetime.datetime | None) -- The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of reduced speed operations. When defined at the environment level or the +port level, if a tugboat, an undefined or later starting date will be +overridden, by default None.

  • +
  • reduced_speed_end (str | datetime.datetime | None) -- The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of reduced speed operations. When defined at the environment level or the +port level, if a tugboat, an undefined or earlier ending date will be +overridden, by default None.

  • +
  • reduced_speed (float) -- The maximum operating speed during the annualized reduced speed operations. +When defined at the environment level, an undefined or faster value will be +overridden, by default 0.0.

  • +
+
+
+
+
+non_operational_dates: DatetimeIndex#
+
+ +
+
+non_operational_dates_set: DatetimeIndex#
+
+ +
+
+reduced_speed_dates: DatetimeIndex#
+
+ +
+
+non_stop_shift: bool#
+
+ +
+
+_validate_threshold(attribute, value)[source]#
+

Ensure a valid threshold is provided for a given strategy.

+
+
Return type:
+

None

+
+
Parameters:
+
    +
  • attribute (Attribute) --

  • +
  • value (int | float) --

  • +
+
+
+
+ +
+ +
+
+

Port Configuration#

+
+
+class wombat.core.data_classes.PortConfig(name, tugboats, crew, n_crews=1, max_operations=1, workday_start=-1, workday_end=-1, site_distance=0.0, annual_fee=0, non_operational_start=None, non_operational_end=None, reduced_speed_start=None, reduced_speed_end=None, reduced_speed=0)[source]#
+

Port configurations for offshore wind power plant scenarios.

+
+
Parameters:
+
    +
  • name (str) -- The name of the port, if multiple are used, then be sure this is unique.

  • +
  • tugboats (list[str]) --

    file, or list of files to create the port's tugboats.

    +
    +

    Note

    +

    Each tugboat is considered to be a tugboat + supporting vessels as +the primary purpose to tow turbines between a repair port and site.

    +
    +

  • +
  • n_crews (int) -- The number of service crews available to be working on repairs simultaneously; +each crew is able to service exactly one repair.

  • +
  • crew (ServiceCrew) -- The crew details, see ServiceCrew for more information. Dictionary +of labor costs with the following: n_day_rate, day_rate, +n_hourly_rate, and hourly_rate.

  • +
  • max_operations (int) -- Total number of turbines the port can handle simultaneously.

  • +
  • workday_start (int) -- The starting hour of a workshift, in 24 hour time.

  • +
  • workday_end (int) -- The ending hour of a workshift, in 24 hour time.

  • +
  • site_distance (int | float) -- Distance, in km, a tugboat has to travel to get between site and port.

  • +
  • annual_fee (int | float) --

    The annualized fee for access to the repair port that will be distributed +monthly in the simulation and accounted for on the first of the month from the +start of the simulation to the end of the simulation.

    +
    +

    Note

    +

    Don't include this cost in both this category and either the +FixedCosts.operations_management_administration bucket or +FixedCosts.marine_management category.

    +
    +

  • +
  • non_operational_start (str | datetime.datetime | None) -- The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of prohibited operations. When defined at the port level, an undefined or +later starting date will be overridden by the environment, and any associated +tubboats will have this value overridden using the same logic, by default None.

  • +
  • non_operational_end (str | datetime.datetime | None) -- The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of prohibited operations. When defined at the port level, an undefined +or earlier ending date will be overridden by the environment, and any associated +tubboats will have this value overridden using the same logic, by default None.

  • +
  • reduced_speed_start (str | datetime.datetime | None) -- The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of reduced speed operations. When defined at the port level, an undefined +or later starting date will be overridden by the environment, and any associated +tubboats will have this value overridden using the same logic, by default None.

  • +
  • reduced_speed_end (str | datetime.datetime | None) -- The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized +period of reduced speed operations. When defined at the port level, an undefined +or earlier ending date will be overridden by the environment, and any associated +tubboats will have this value overridden using the same logic, by default None.

  • +
  • reduced_speed (float) -- The maximum operating speed during the annualized reduced speed operations. +When defined at the port level, an undefined or faster value will be overridden +by the environment, and any associated tubboats will have this value overridden +using the same logic, by default 0.0.

  • +
+
+
+
+
+non_operational_dates: DatetimeIndex#
+
+ +
+
+reduced_speed_dates: DatetimeIndex#
+
+ +
+ +
+
+
+

Wind Farm Support#

+
+

Subassembly Model#

+
+
+class wombat.core.data_classes.SubassemblyData(name, maintenance, failures, system_value, rng)[source]#
+

Data storage and validation class for the subassemblies.

+
+
Parameters:
+
    +
  • name (str) -- Name of the component/subassembly.

  • +
  • maintenance (list[dict[str, float | str]]) -- List of the maintenance classification dictionaries. This will be converted +to a list of Maintenance objects in the post initialization hook.

  • +
  • failures (dict[int, dict[str, float | str]]) -- Dictionary of failure classifications in a numerical (ordinal) categorization +order. This will be converted to a dictionary of Failure objects in the +post initialization hook.

  • +
  • system_value (int | float) -- Turbine's cost of replacement. Used in case percentages of turbine cost are used +in place of an absolute cost.

  • +
  • rng (Generator) --

  • +
+
+
+
+ +
+
+

Wind Farm Map#

+
+
+class wombat.core.data_classes.WindFarmMap(substation_map, export_cables)[source]#
+

A list of the upstream connections for a turbine and its downstream connector.

+
+
Parameters:
+
    +
  • substation_map (list[str]) -- A dictionary mapping of each substation and its SubstationMap.

  • +
  • export_cables (list[tuple[str, str]]) -- A list of the export cable connections.

  • +
+
+
+
+
+get_upstream_connections(substation, string_start, node, return_cables=True)[source]#
+

Retrieve the upstream turbines (and optionally cables) within the wind farm +graph.

+
+
Parameters:
+
    +
  • substation (str) -- The substation's System.id.

  • +
  • string_start (str) -- The System.id of the first turbine in the string.

  • +
  • node (str) -- The System.id of the ending node for a cable connection.

  • +
  • return_cables (bool) -- Indicates if the Cable.id should be generated for each of the turbines, +by default True.

  • +
+
+
Return type:
+

list[str] | tuple[list[str], list[str]]

+
+
Returns:
+

list[str] | tuple[list[str], list[str]] -- A list of System.id for all of the upstream turbines of node if +cables=False, otherwise the upstream turbine and the Cable.id lists +are returned.

+
+
+
+ +
+
+get_upstream_connections_from_substation(substation, return_cables=True, by_string=True)[source]#
+

Retrieve the upstream turbines (and optionally, cables) connected to a +py:attr:substation in the wind farm graph.

+
+
Parameters:
+
    +
  • substation (str) -- The py:attr:System.id for the substation.

  • +
  • return_cables (bool, optional) -- Indicates if the Cable.id should be generated for each of the turbines, +by default True

  • +
  • by_string (bool, optional) -- Indicates if the list of turbines (and cables) should be a nested list for +each string (py:obj:True), or as 1-D list (py:obj:False), by default +True.

  • +
+
+
Return type:
+

list[str] | tuple[list[str], list[str]] | list[list[str]] | tuple[list[list[str]], list[list[str]]]

+
+
Returns:
+

list[str] | tuple[list[str], list[str]] -- A list of System.id for all of the upstream turbines of node if +return_cables=False, otherwise the upstream turbine and the Cable.id +lists are returned. These are bifurcated in lists of lists for each string +if by_string=True

+
+
+
+ +
+ +
+
+

Substation Map#

+
+
+class wombat.core.data_classes.SubstationMap(string_starts, string_map, downstream)[source]#
+

A mapping of every String connected to a substation, excluding export +connections to other substations.

+
+
Parameters:
+
    +
  • string_starts (list[str]) -- A list of every first turbine's System.id in a string connected to the +substation.

  • +
  • string_map (dict[str, String]) -- A dictionary mapping each string starting turbine to its String data.

  • +
  • downstream (str) -- The System.id of where the export cable leads. This should be the same +System.id as the substation for an interconnection point, or another +connecting substation.

  • +
+
+
+
+ +
+
+

String#

+
+
+class wombat.core.data_classes.String(start, upstream_map)[source]#
+

All of the connection information for a complete string in a wind farm.

+
+
Parameters:
+
    +
  • start (str) -- The substation's ID (System.id)

  • +
  • upstream_map (dict[str, SubString]) -- The dictionary of each turbine ID in the string and it's upstream SubString.

  • +
+
+
+
+ +
+
+

Sub String#

+
+
+class wombat.core.data_classes.SubString(downstream, upstream)[source]#
+

A list of the upstream connections for a turbine and its downstream connector.

+
+
Parameters:
+
    +
  • downstream (str) -- The downstream turbine/substation connection id.

  • +
  • upstream (list[str]) -- A list of the upstream turbine connections.

  • +
+
+
+
+ +
+
+
+

Miscellaneous#

+
+

Fixed Cost Model#

+
+
+class wombat.core.data_classes.FixedCosts(operations=0, operations_management_administration=0, project_management_administration=0, marine_management=0, weather_forecasting=0, condition_monitoring=0, operating_facilities=0, environmental_health_safety_monitoring=0, insurance=0, brokers_fee=0, operations_all_risk=0, business_interruption=0, third_party_liability=0, storm_coverage=0, annual_leases_fees=0, submerge_land_lease_costs=0, transmission_charges_rights=0, onshore_electrical_maintenance=0, labor=0)[source]#
+

Fixed costs for operating a windfarm. All values are assumed to be in $/kW/yr.

+
+
Parameters:
+
    +
  • operations (float) -- Non-maintenance costs of operating the project. If a value is provided for this +attribute, then it will zero out all other values, otherwise it will be set to +the sum of the remaining values.

  • +
  • operations_management_administration (float) --

    Activities necessary to forecast, dispatch, sell, and manage the production of +power from the plant. Includes both the onsite and offsite personnel, software, +and equipment to coordinate high voltage equipment, switching, port activities, +and marine activities.

    +
    +

    Note

    +

    This should only be used when not breaking down the cost into the +following categories: project_management_administration, +operation_management_administration, marine_management, and/or +weather_forecasting

    +
    +

  • +
  • project_management_administration (float) -- Financial reporting, public relations, procurement, parts and stock management, +H&SE management, training, subcontracts, and general administration.

  • +
  • marine_management (float) -- Coordination of port equipment, vessels, and personnel to carry out inspections +and maintenance of generation and transmission equipment.

  • +
  • weather_forecasting (float) -- Daily forecast of metocean conditions used to plan maintenance visits and +estimate project power production.

  • +
  • condition_monitoring (float) -- Monitoring of SCADA data from wind turbine components to optimize performance +and identify component faults.

  • +
  • operating_facilities (float) -- Co-located offices, parts store, quayside facilities, helipad, refueling +facilities, hanger (if necesssary), etc.

  • +
  • environmental_health_safety_monitoring (float) -- Coordination and monitoring to ensure compliance with HSE requirements during +operations.

  • +
  • insurance (float) --

    Insurance policies during operational period including All Risk Property, +Buisness Interuption, Third Party Liability, and Brokers Fee, and Storm +Coverage.

    +
    +

    Note

    +

    This should only be used when not breaking down the cost into the +following categories: brokers_fee, operations_all_risk, +business_interruption, third_party_liability, and/or +storm_coverage

    +
    +

  • +
  • brokers_fee (float) -- Fees for arranging the insurance package.

  • +
  • operations_all_risk (float) -- All Risk Property (physical damage). Sudden and unforseen physical loss or +physical damage to teh plant/assets during the operational phase of a project.

  • +
  • business_interruption (float) -- Sudden and unforseen loss or physical damage to the plant/assets during the +operational phase of a project causing an interruption.

  • +
  • third_party_liability (float) -- Liability imposed by law, and/or Express Contractual Liability, for bodily +injury or property damage.

  • +
  • storm_coverage (float) -- Coverage from huricane and tropical storm events (tyipcally for Atlantic Coast +projects).

  • +
  • annual_leases_fees (float) --

    Ongoing payments, including but not limited to: payments to regulatory body for +permission to operate at project site (terms defined within lease); payments to +Transmission Systems Operators or Transmission Asseet Owners for rights to +transport generated power.

    +
    +

    Note

    +

    This should only be used when not breaking down the cost into the +following categories: submerge_land_lease_costs and/or +transmission_charges_rights

    +
    +

  • +
  • submerge_land_lease_costs (float) -- Payments to submerged land owners for rights to build project during operations.

  • +
  • transmission_charges_rights (float) -- Any payments to Transmissions Systems Operators or Transmission Asset Owners for +rights to transport generated power.

  • +
  • onshore_electrical_maintenance (float) --

    Inspections of cables, transformer, switch gears, power compensation equipment, +etc. and infrequent repairs

    +
    +

    Warning

    +

    This should only be used if not modeling these as processes within +the model. Currently, onshore modeling is not included.

    +
    +

  • +
  • labor (float) -- The costs associated with labor, if not being modeled through the simulated +processes.

  • +
+
+
+
+ +
+
+
+ + + + +
+ + + + + + + + +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/API/utilities.html b/API/utilities.html new file mode 100644 index 00000000..646ba49e --- /dev/null +++ b/API/utilities.html @@ -0,0 +1,924 @@ + + + + + + + + + + + Helpers and Plotting — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Helpers and Plotting#

+
+

Plotting#

+

Provides expoerimental plotting routines to help with simulation diagnostics.

+
+
+wombat.utilities.plot.plot_farm_layout(windfarm, figure_kwargs=None, plot_kwargs=None, return_fig=False)[source]#
+

Plot the graph representation of the windfarm as represented through WOMBAT.

+
+
Args:
+
figure_kwargsdict, optional

Customized keyword arguments for matplotlib figure instantiation that +will passed as plt.figure(**figure_kwargs). Defaults to {}.

+
+
plot_kwargsdict, optional

Customized parameters for networkx.draw() that can will passed as +nx.draw(**figure_kwargs). Defaults to with_labels=True, +horizontalalignment=right, verticalalignment=bottom, +font_weight=bold, font_size=10, and node_color=#E37225.

+
+
return_figbool, optional

Whether or not to return the figure and axes objects for further editing +and/or saving. Defaults to False.

+
+
+
+
+
+
Return type:
+

None | tuple[figure, axes]

+
+
Returns:
+

None | tuple[plt.figure, plt.axes]: _description_

+
+
Parameters:
+
    +
  • windfarm (Windfarm) --

  • +
  • figure_kwargs (dict | None) --

  • +
  • plot_kwargs (dict | None) --

  • +
  • return_fig (bool) --

  • +
+
+
+
+ +
+
+wombat.utilities.plot.plot_farm_availability(sim, which='energy', individual_turbines=False, farm_95_CI=False, figure_kwargs=None, plot_kwargs=None, legend_kwargs=None, tick_fontsize=12, label_fontsize=16, return_fig=False)[source]#
+

Plots a line chart of the monthly availability at the wind farm level.

+
+
Parameters:
+
    +
  • sim (Simulation) -- A Simulation object that has been run.

  • +
  • which (str) -- One of "time" or "energy", to indicate the basis for the availability +calculation, by default "energy".

  • +
  • individual_turbines (bool, optional) -- Indicates if faint gray lines should be added in the background for the +availability of each turbine, by default False.

  • +
  • farm_95_CI (bool, optional) -- Indicates if the 95% CI area fill should be added in the background.

  • +
  • figure_kwargs (dict, optional) -- Custom parameters for plt.figure(), by default figsize=(15, 7) and +dpi=300.

  • +
  • plot_kwargs (dict, optional) -- Custom parameters to be passed to ax.plot(), by default a label consisting +of the simulation name and project-level availability.

  • +
  • legend_kwargs (dict, optional) -- Custom parameters to be passed to ax.legend(), by default fontsize=14.

  • +
  • tick_fontsize (int, optional) -- The x- and y-axis tick label fontsize, by default 12.

  • +
  • label_fontsize (int, optional) -- The x- and y-axis label fontsize, by default 16.

  • +
  • return_fig (bool, optional) -- If True, return the figure and Axes object, otherwise don't, by default +False.

  • +
+
+
Return type:
+

tuple[Figure | Axes] | None

+
+
Returns:
+

tuple[plt.Figure, plt.Axes] | None -- See return_fig for details. +_description_

+
+
+
+ +
+
+wombat.utilities.plot.plot_operational_levels(sim, figure_kwargs=None, cbar_label_fontsize=14, return_fig=False)[source]#
+

Plots an hourly view of the operational levels of the wind farm and individual +turbines as a heatmap.

+
+
Parameters:
+
    +
  • sim (Simulation) -- A Simulation object that has been run.

  • +
  • figure_kwargs (dict, optional) -- Custom settings for plt.figure(), by default figsize=(15, 10) +and dpi=300.

  • +
  • cbar_label_fontsize (int, optional) -- The default fontsize used in the color bar legend for the axis label, by default +14.

  • +
  • return_fig (bool, optional) -- If True, return the figure and Axes object, otherwise don't, by default +False.

  • +
+
+
Returns:
+

tuple[plt.Figure, plt.Axes] | None -- See return_fig for details.

+
+
+
+ +
+
+

Logging functions#

+

General logging methods.

+
+
+wombat.utilities.logging.setup_logger(logger_name, log_file, level=20, capacity=500)[source]#
+

Creates the logging infrastructure for a given logging category.

+

TODO: Figure out how to type check logging.INFO; Callable?

+
+
Parameters:
+
    +
  • logger_name (str) -- Name to assign to the logger.

  • +
  • log_file (Path) -- File name and path for where the log data should be saved.

  • +
  • level (Any, optional) -- Logging level, by default logging.INFO.

  • +
  • capacity (int) --

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+wombat.utilities.logging.format_events_log_message(simulation_time, env_time, system_id, system_name, part_id, part_name, system_ol, part_ol, agent, action, reason, additional, duration, request_id, location='na', materials_cost=0, hourly_labor_cost=0, salary_labor_cost=0, equipment_cost=0)[source]#
+

Formats the logging messages into the expected format for logging.

+
+
Parameters:
+
    +
  • simulation_time (datetime64) -- Timestamp within the simulation time.

  • +
  • env_time (float) -- Environment simulation time (Environment.now).

  • +
  • system_id (str) -- Turbine ID, System.id.

  • +
  • system_name (str) -- Turbine name, System.name.

  • +
  • part_id (str) -- Subassembly, component, or cable ID, _.id.

  • +
  • part_name (str) -- Subassembly, component, or cable name, _.name.

  • +
  • system_ol (int | float) -- System operating level, System.operating_level. Use an empty string for n/a.

  • +
  • part_ol (int | float) -- Subassembly, component, or cable operating level, _.operating_level. Use an +empty string for n/a.

  • +
  • agent (str) -- Agent performin the action.

  • +
  • action (str) -- Action that was taken.

  • +
  • reason (str) -- Reason an action was taken.

  • +
  • additional (str) -- Any additional information that needs to be logged.

  • +
  • duration (float) -- Length of time the action lasted.

  • +
  • request_id (str) -- The RepairRequest.request_id or "na".

  • +
  • location (str) -- The location of where the event ocurred: should be one of site, port, +enroute, or system, by default "na".

  • +
  • materials_cost (int | float, optional) -- Total cost of materials for action, in USD, by default 0.

  • +
  • hourly_labor_cost (int | float, optional) -- Total cost of hourly labor for action, in USD, by default 0.

  • +
  • salary_labor_cost (int | float, optional) -- Total cost of salaried labor for action, in USD, by default 0.

  • +
  • equipment_cost (int | float, optional) -- Total cost of equipment for action, in USD, by default 0.

  • +
+
+
Return type:
+

str

+
+
Returns:
+

str -- Formatted message for consistent logging.[summary]

+
+
+
+ +
+
+

Time Calculations#

+

General methods for time-based calculations.

+
+
+wombat.utilities.time.parse_date(value)[source]#
+

Thin wrapper for dateutil.parser.parse that converts string dates and returns +back None or the original value if it's None or a datetime.datetime object, +respectively.

+
+
Parameters:
+

value (str | None | datetime.datetime) -- A month/date or month-date formatted string to be converted to a +datetime.datetime object, or datetime.datetime object, or None.

+
+
Return type:
+

datetime | None

+
+
Returns:
+

datetime.datetime | None -- A converted datetime.datetime object or None.

+
+
+
+ +
+
+wombat.utilities.time.convert_dt_to_hours(diff)[source]#
+

Convert a datetime.timedelta object to number of hours at the seconds +resolution.

+
+
Parameters:
+

diff (datetime.timedelta) -- The difference between two datetime.datetime objects.

+
+
Return type:
+

float

+
+
Returns:
+

float -- Number of hours between to datetime.datetime objects.

+
+
+
+ +
+
+wombat.utilities.time.hours_until_future_hour(dt, hour)[source]#
+

Number of hours until a future hour in the same day for hour <= 24, +otherwise, it is the number of hours until a time in the proceeding days.

+
+
Parameters:
+
    +
  • dt (datetime.datetime) -- Focal datetime.

  • +
  • hour (int) -- Hour that is later in the day, in 24 hour time.

  • +
+
+
Return type:
+

float

+
+
Returns:
+

float -- Number of hours between the two times.

+
+
+
+ +
+
+wombat.utilities.time.check_working_hours(env_start, env_end, workday_start, workday_end)[source]#
+

Checks the working hours of a port or servicing equipment, and overrides a +default (-1) to the environment's settings, otherwise returns back the input hours.

+
+
Parameters:
+
    +
  • env_start (int) -- The starting hour for the environment's shift

  • +
  • env_end (int) -- The ending hour for the environment's shift

  • +
  • workday_start (int) -- The starting hour to be checked.

  • +
  • workday_end (int) -- The ending hour to be checked.

  • +
+
+
Return type:
+

tuple[int, int]

+
+
Returns:
+

tuple[int, int] -- The starting and ending hour to be applied back to the port or servicing +equipment.

+
+
+
+ +
+
+wombat.utilities.time.calculate_cost(duration, rate, n_rate=1, daily_rate=False)[source]#
+

Calculates the equipment cost, or labor cost for either salaried or hourly +employees.

+
+
Parameters:
+
    +
  • duration (int | float) -- Length of time, in hours.

  • +
  • rate (float) -- The labor or equipment rate, in $USD/hour.

  • +
  • n_rate (int) -- Total number of of the rate to be applied, more than one if``rate`` is broken +down by number of individual laborers (if rate is a labor rate), by default 1.

  • +
  • daily_rate (bool, optional) -- Indicator for if the rate is a daily rate (True), or hourly rate (False), by +default False.

  • +
+
+
Return type:
+

float

+
+
Returns:
+

float -- The total cost of the labor performed.

+
+
+
+ +
+
+

Miscellaneous#

+

Provides various utility functions that don't fit within a common theme.

+
+
+wombat.utilities.utilities._mean(*args)[source]#
+

Multiplies two numbers. Used for a reduce operation.

+
+
Parameters:
+

args (int | float) -- The values to compute the mean over

+
+
Return type:
+

float

+
+
Returns:
+

float -- The average of the values provided

+
+
+
+ +
+
+wombat.utilities.utilities.create_variable_from_string(string)[source]#
+

Creates a valid Python variable style string from a passed string.

+
+
Parameters:
+

string (str) -- The string to convert into a Python-friendly variable name.

+
+
Return type:
+

str

+
+
Returns:
+

str -- A Python-valid variable name.

+
+
+

Examples

+
>>> example_string = "*Electrical!*_ _System$*_"
+>>> print(create_variable_from_string(example_string))
+'electrical_system'
+
+
+
+ +
+
+wombat.utilities.utilities.IEC_power_curve(windspeed_column, power_column, bin_width=0.5, windspeed_start=0.0, windspeed_end=30.0)[source]#
+

Direct copyfrom OpenOA: +NREL/OpenOA +Use IEC 61400-12-1-2 method for creating wind-speed binned power curve.

+
+
Parameters:
+
    +
  • windspeed_column (np.ndarray | pandas.Series) -- The power curve's windspeed values, in m/s.

  • +
  • power_column (np.ndarray | pandas.Series) -- The power curve's output power values, in kW.

  • +
  • bin_width (float) -- Width of windspeed bin, default is 0.5 m/s according to standard, by default +0.5.

  • +
  • windspeed_start (float) -- Left edge of first windspeed bin, where all proceeding values will be 0.0, +by default 0.0.

  • +
  • windspeed_end (float) -- Right edge of last windspeed bin, where all following values will be 0.0, by +default 30.0.

  • +
+
+
Return type:
+

Callable

+
+
Returns:
+

+
Callable

Python function of the power curve, of type (Array[float] -> Array[float]), +that maps input windspeed value(s) to ouptut power value(s).

+
+
+

+
+
+
+ +
+
+ + + + +
+ + + + + + + + +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/API/windfarm.html b/API/windfarm.html new file mode 100644 index 00000000..afefb22e --- /dev/null +++ b/API/windfarm.html @@ -0,0 +1,1275 @@ + + + + + + + + + + + Wind Farm Classes — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + + + + +
+ +
+

Wind Farm Classes#

+

The wind farm classes define how the wind farm's graph model are created, and power the +timing out of the failures and maintenance events within the simulation for the cables, +substations, and turbines.

+
+

Wind Farm#

+

Creates the Windfarm class/model.

+
+
+class wombat.windfarm.windfarm.Windfarm(env, windfarm_layout, repair_manager)[source]#
+

The primary class for operating on objects within a windfarm. The substations, +cables, and turbines are created as a network object to be more appropriately +accessed and controlled.

+
+
Parameters:
+
+
+
+
+
+_create_graph_layout(windfarm_layout)[source]#
+

Creates a network layout of the windfarm start from the substation(s) to +be able to capture downstream turbines that can be cut off in the event of a +cable failure.

+
+
Parameters:
+

windfarm_layout (str) -- Filename to use for reading in the windfarm layout; must be a csv file.

+
+
Return type:
+

None

+
+
+
+ +
+
+_create_turbines_and_substations()[source]#
+

Instantiates the turbine and substation models as defined in the +user-provided layout file, and connects these models to the appropriate graph +nodes to create a fully representative windfarm network model.

+
+
Raises:
+

ValueError -- Raised if the subassembly data is not provided in the layout file.

+
+
Return type:
+

None

+
+
+
+ +
+
+_create_cables()[source]#
+

Instantiates the cable models as defined in the user-provided layout file, +and connects these models to the appropriate graph edges to create a fully +representative windfarm network model.

+
+
Raises:
+

ValueError -- Raised if the cable model is not specified.

+
+
Return type:
+

None

+
+
+
+ +
+
+calculate_distance_matrix()[source]#
+

Calculates the geodesic distance, in km, between all of the windfarm's nodes, +e.g., substations and turbines, and cables.

+
+
Return type:
+

None

+
+
+
+ +
+
+_create_substation_turbine_map()[source]#
+

Creates substation_turbine_map, a dictionary, that maps substation(s) to +the dependent turbines in the windfarm, and the weighting of each turbine in the +windfarm.

+
+
Return type:
+

None

+
+
+
+ +
+
+_create_wind_farm_map()[source]#
+

Creates a secondary graph object strictly for traversing the windfarm to turn +on/off the appropriate turbines, substations, and cables more easily.

+
+
Return type:
+

None

+
+
+
+ +
+
+finish_setup()[source]#
+

Final initialization hook for any substations, turbines, or cables.

+
+
Return type:
+

None

+
+
+
+ +
+
+_setup_logger(initial=True)[source]#
+
+
Parameters:
+

initial (bool) --

+
+
+
+ +
+
+_log_operations()[source]#
+

Logs the operational data for a simulation.

+
+ +
+
+system(system_id)[source]#
+

Convenience function to returns the desired System object for a turbine or +substation in the windfarm.

+
+
Parameters:
+

system_id (str) -- The system's unique identifier, wombat.windfarm.System.id.

+
+
Return type:
+

System

+
+
Returns:
+

System -- The System object.

+
+
+
+ +
+
+cable(cable_id)[source]#
+

Convenience function to returns the desired Cable object for a cable in the +windfarm.

+
+
Parameters:
+

cable_id (tuple[str, str] | str) -- The cable's unique identifier, of the form: (wombat.windfarm.System.id, +wombat.windfarm.System.id), for the (downstream node id, upstream node +id), or the Cable.id.

+
+
Return type:
+

Cable

+
+
Returns:
+

Cable -- The Cable object.

+
+
+
+ +
+
+property current_availability: float#
+

Calculates the product of all system operating_level variables across +the windfarm using the following forumation.

+
+\[\sum{ + OperatingLevel_{substation_{i}} * + \sum{OperatingLevel_{turbine_{j}} * Weight_{turbine_{j}}} +}\]
+

where the :math:{OperatingLevel} is the product of the operating level +of each subassembly on a given system (substation or turbine), and the +:math:{Weight} is the proportion of one turbine's capacity relative to +the whole windfarm.

+
+ +
+
+property current_availability_wo_servicing: float#
+

Calculates the product of all system operating_level variables across +the windfarm using the following forumation, ignoring 0 operating level due to +ongoing servicing.

+
+\[\sum{ + OperatingLevel_{substation_{i}} * + \sum{OperatingLevel_{turbine_{j}} * Weight_{turbine_{j}}} +}\]
+

where the :math:{OperatingLevel} is the product of the operating level +of each subassembly on a given system (substation or turbine), and the +:math:{Weight} is the proportion of one turbine's capacity relative to +the whole windfarm.

+
+ +
+ +
+
+

System: Wind turbine or substation#

+

Creates the Turbine class.

+
+
+class wombat.windfarm.system.system.System(env, repair_manager, t_id, name, subassemblies, system)[source]#
+

Can either be a turbine or substation, but is meant to be something that consists +of 'Subassembly' pieces.

+

See here +for more information.

+
+
Parameters:
+
+
+
+
+
+_calculate_system_value(subassemblies)[source]#
+

Calculates the turbine's value based its capex_kw and capacity.

+
+
Parameters:
+
    +
  • system (str) -- One of "turbine" or "substation".

  • +
  • subassemblies (dict) -- Dictionary of subassemblies.

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+_create_subassemblies(subassembly_data, system)[source]#
+

Creates each subassembly as a separate attribute and also a list for quick +access.

+
+
Parameters:
+
    +
  • subassembly_data (dict) -- Dictionary providing the maintenance and failure definitions for at least +one subassembly named

  • +
  • system (str) -- One of "turbine" or "substation" to indicate if the power curves should also +be created, or not.

  • +
+
+
Return type:
+

None

+
+
+
+ +
+
+_initialize_power_curve(power_curve_dict)[source]#
+

Creates the power curve function based on the power_curve input in the +subassembly_data dictionary. If there is no valid input, then 0 will always +be reutrned.

+
+
Parameters:
+

power_curve_dict (dict) -- The turbine definition dictionary.

+
+
Return type:
+

None

+
+
+
+ +
+
+interrupt_all_subassembly_processes(origin=None)[source]#
+

Interrupts the running processes in all of the system's subassemblies.

+
+
Parameters:
+

origin (Subassembly) -- The subassembly that triggered the request, if the method call is coming +from a subassembly shutdown event.

+
+
Return type:
+

None

+
+
+
+ +
+
+property operating_level: float#
+

The turbine's operating level, based on subassembly and cable performance.

+
+
Returns:
+

float -- Operating level of the turbine.

+
+
+
+ +
+
+property operating_level_wo_servicing: float#
+

The turbine's operating level, based on subassembly and cable performance, +without accounting for servicing status.

+
+
Returns:
+

float -- Operating level of the turbine.

+
+
+
+ +
+
+power(windspeed)[source]#
+

Generates the power output for an iterable of windspeed values.

+
+
Parameters:
+

windspeed (list[float] | np.ndarrays) -- Windspeed values, in m/s.

+
+
Return type:
+

ndarray

+
+
Returns:
+

np.ndarray -- Power production, in kW.

+
+
+
+ +
+ +
+
+

Subassembly: The modeled components of a system#

+

Provides the Subassembly class.

+
+
+class wombat.windfarm.system.subassembly.Subassembly(system, env, s_id, subassembly_data)[source]#
+

A major system composes the turbine or substation objects.

+
+
Parameters:
+
+
+
+
+
+_create_processes()[source]#
+

Creates the processes for each of the failure and maintenance types.

+
+
Yields:
+

Tuple[Union[str, int], simpy.events.Process] -- Creates a dictionary to keep track of the running processes within the +subassembly.

+
+
+
+ +
+
+recreate_processes()[source]#
+

If a turbine is being reset after a tow-to-port repair or replacement, then +all processes are assumed to be reset to 0, and not pick back up where they left +off.

+
+
Return type:
+

None

+
+
+
+ +
+
+interrupt_processes(origin=None)[source]#
+

Interrupts all of the running processes within the subassembly except for the +process associated with failure that triggers the catastrophic failure.

+
+
Parameters:
+

origin (Subassembly) -- The subassembly that triggered the request, if the method call is coming +from a subassembly shutdown event. If provided, and it is the same as the +current subassembly, then a try/except flow is used to ensure the process +that initiated the shutdown is not interrupting itself.

+
+
Return type:
+

None

+
+
+
+ +
+
+interrupt_all_subassembly_processes()[source]#
+

Thin wrapper for system.interrupt_all_subassembly_processes.

+
+
Return type:
+

None

+
+
+
+ +
+
+trigger_request(action)[source]#
+

Triggers the actual repair or maintenance logic for a failure or maintenance +event, respectively.

+
+
Parameters:
+

action (Maintenance | Failure) -- The maintenance or failure event that triggers a RepairRequest.

+
+
+
+ +
+
+run_single_maintenance(maintenance)[source]#
+

Runs a process to trigger one type of maintenance request throughout the +simulation.

+
+
Parameters:
+

maintenance (Maintenance) -- A maintenance category.

+
+
Yields:
+

simpy.events. HOURS_IN_DAY -- Time between maintenance requests.

+
+
Return type:
+

Generator

+
+
+
+ +
+
+run_single_failure(failure)[source]#
+

Runs a process to trigger one type of failure repair request throughout the +simulation.

+
+
Parameters:
+

failure (Failure) -- A failure classification.

+
+
Yields:
+

simpy.events. HOURS_IN_DAY -- Time between failure events that need to request a repair.

+
+
Return type:
+

Generator

+
+
+
+ +
+ +
+
+

Cable: Hybrid system and subassembly model#

+

"Defines the Cable class and cable simulations.

+
+
+class wombat.windfarm.system.cable.Cable(windfarm, env, connection_type, start_node, end_node, cable_data, name=None)[source]#
+

The cable system/asset class.

+
+
Parameters:
+
    +
  • windfarm (wombat.windfarm.Windfarm) -- The Windfarm object.

  • +
  • env (WombatEnvironment) -- The simulation environment.

  • +
  • cable_id (str) -- The unique identifier for the cable.

  • +
  • connection_type (str) -- The type of cable. Must be one of "array" or "export".

  • +
  • start_node (str) -- The starting point (system.id) (turbine or substation) of the cable segment.

  • +
  • cable_data (dict) -- The dictionary defining the cable segment.

  • +
  • end_node (str) --

  • +
  • name (str | None) --

  • +
+
+
+
+
+set_string_details(start_node, substation)[source]#
+

Sets the starting turbine for the string to be used for traversing the +correct upstream connections when resetting after a failure.

+
+
Parameters:
+
    +
  • start_node (str) -- The System.id for the starting turbine on a string.

  • +
  • substation (str) -- The System.id for the string's connecting substation.

  • +
+
+
+
+ +
+
+finish_setup()[source]#
+

Creates the upstream_nodes and upstream_cables attributes for use by +the cable when triggering usptream failures and resetting them after the repair +is complete.

+
+
Return type:
+

None

+
+
+
+ +
+
+_create_processes()[source]#
+

Creates the processes for each of the failure and maintenance types.

+
+
Yields:
+

Tuple[Union[str, int], simpy.events.Process] -- Creates a dictionary to keep track of the running processes within the +subassembly.

+
+
+
+ +
+
+recreate_processes()[source]#
+

If a cable is being reset after a replacement, then all processes are +assumed to be reset to 0, and not pick back up where they left off.

+
+
Return type:
+

None

+
+
+
+ +
+
+interrupt_processes()[source]#
+

Interrupts all of the running processes within the subassembly except for the +process associated with failure that triggers the catastrophic failure.

+
+
Parameters:
+

subassembly (Subassembly) -- The subassembly that should have all processes interrupted.

+
+
Return type:
+

None

+
+
+
+ +
+
+interrupt_all_subassembly_processes()[source]#
+

Thin wrapper for interrupt_processes for consistent usage with system.

+
+
Return type:
+

None

+
+
+
+ +
+
+stop_all_upstream_processes(failure)[source]#
+

Stops all upstream turbines and cables from producing power by creating a +env.event() for each System.cable_failure and +Cable.downstream_failure, respectively. In the case of an export cable, each +string is traversed to stop the substation and upstream turbines and cables.

+
+
Parameters:
+

failure (Failre) -- The Failure that is causing a string shutdown.

+
+
Return type:
+

None

+
+
+
+ +
+
+trigger_request(action)[source]#
+

Triggers the actual repair or maintenance logic for a failure or maintenance +event, respectively.

+
+
Parameters:
+

action (Maintenance | Failure) -- The maintenance or failure event that triggers a RepairRequest.

+
+
+
+ +
+
+run_single_maintenance(maintenance)[source]#
+

Runs a process to trigger one type of maintenance request throughout the +simulation.

+
+
Parameters:
+

maintenance (Maintenance) -- A maintenance category.

+
+
Yields:
+

simpy.events.Timeout -- Time between maintenance requests.

+
+
Return type:
+

Generator

+
+
+
+ +
+
+run_single_failure(failure)[source]#
+

Runs a process to trigger one type of failure repair request throughout the +simulation.

+
+
Parameters:
+

failure (Failure) -- A failure classification.

+
+
Yields:
+

simpy.events.Timeout -- Time between failure events that need to request a repair.

+
+
Return type:
+

Generator

+
+
+
+ +
+ +
+
+ + + + +
+ + + + + + + + +
+ + + + + + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_downloads/30aaf67e9b6bf27387b51f7cf6267503/operations_and_maintenance_model_FY20.pdf b/_downloads/30aaf67e9b6bf27387b51f7cf6267503/operations_and_maintenance_model_FY20.pdf new file mode 100644 index 00000000..0d411a2d Binary files /dev/null and b/_downloads/30aaf67e9b6bf27387b51f7cf6267503/operations_and_maintenance_model_FY20.pdf differ diff --git a/_downloads/5d8e6320b3861dbde9632c4932c79584/nawea_2023_wombat_tutorial_87913.pdf b/_downloads/5d8e6320b3861dbde9632c4932c79584/nawea_2023_wombat_tutorial_87913.pdf new file mode 100644 index 00000000..b8290b93 Binary files /dev/null and b/_downloads/5d8e6320b3861dbde9632c4932c79584/nawea_2023_wombat_tutorial_87913.pdf differ diff --git a/_downloads/8270ee20ee3e039689ef51ef547e2cd2/WOMBAT_IEA_task_26_presentation_6_May_2021.pdf b/_downloads/8270ee20ee3e039689ef51ef547e2cd2/WOMBAT_IEA_task_26_presentation_6_May_2021.pdf new file mode 100644 index 00000000..154cee48 Binary files /dev/null and b/_downloads/8270ee20ee3e039689ef51ef547e2cd2/WOMBAT_IEA_task_26_presentation_6_May_2021.pdf differ diff --git a/_downloads/a10d3e8d99c36b7c62a9aff85058bb48/code_comparison.pdf b/_downloads/a10d3e8d99c36b7c62a9aff85058bb48/code_comparison.pdf new file mode 100644 index 00000000..491ce19c Binary files /dev/null and b/_downloads/a10d3e8d99c36b7c62a9aff85058bb48/code_comparison.pdf differ diff --git a/_images/class_diagram.svg b/_images/class_diagram.svg new file mode 100644 index 00000000..62602233 --- /dev/null +++ b/_images/class_diagram.svg @@ -0,0 +1 @@ +FromDictMixinfrom_dict(cls, data)Failurelevel: intscale: floatshape: floattime: floatmaterials: floatdescription: stroperation_reduction: floatservice_equipment: list[str]system_value: floatweibullrequest_id: strassign_id(request_id: str)hours_to_next_failure() : floatMaintenancetime: floatmaterials: floatdescription: stroperation_reduction: floatservice_equipment: list[str]system_value: floatrequest_id: strassign_id(request_id: str)RepairRequestcable: booldetails: Failure | Maintenancerequest_id: strseverity_level: intsubassembly_id: strsubassembly_name: strsystem_id: strsystem_name: strupstream_turbines: list[str]assign_id(request_id: str)ServiceEquipmentDatadata_dict: dictstrategy: strdetermine_type()ServiceCrewday_rate: floathourly_rate: floatn_day_rate: intn_hourly_rate: intScheduledServiceEquipmentDataname: strcapability: list[str]equipment_rate: floatcrew: ServiceCrewn_crews: intcrew_transfer_time: floatstart_day: intstart_month: intend_day: intend_month: intstart_year: intend_year: intstrategy: stronsite: boolworkday_start: intworkday_end: intmethod: strmobilization_cost: floatmobilization_days: intspeed: floatmax_waveheight_repair: floatmax_waveheight_transport: floatmax_windspeed_repair: floatmax_windspeed_transport: floatoperating_dates: numpy.ndarraycreate_date_range()UnscheduledServiceEquipmentDataname: strcapability: list[str]equipment_rate: floatcrew: ServiceCrewn_crews: intcrew_transfer_time: floatcharter_days: intstrategy: strstrategy_threshold: int | floatworkday_start: intworkday_end: intmethod: strmobilization_cost: floatmobilization_days: intspeed: floattow_sped: floatspeed_reduction_factor: floatmax_waveheight_repair: floatmax_waveheight_transport: floatmax_windspeed_repair: floatmax_windspeed_transport: floatport_distance: floatunmoor_hours: floatreconnection_hours: floatonsite: boolPortConfigname: strtugboats: list[UnscheduledServiceEquipmentData]crew: ServiceCrewn_crews: intmax_operations: intworkday_start: intworkday_end: intsite_distance: floatannual_fee: floatSubassemblyDataname: strsystem_value: floatfailures: dict[int, Failure]maintenance: list[Maintenance]FixedCostsoperations: floatannual_lease_fees: floatbrokers_fee: floatbusiness_interruption: floatcondition_monitoring: floatenvironmental_health_safety_monitoring: floatinsurance: floatlabor: floatmarine_management: floatonshore_electrical_maintenance: floatoperating_facilities: floatoperations_all_risk: floatoperations_management_administration: floatproject_management_administration: floatstorm_coverage: floatsubmerge_land_lease_costs: floatthird_party_liability: floattransmission_charges_rights: floatweather_forecasting: floathierarchy: dictresolution: dictcost_category_validator(name: str, sub_name: list[str])Portenv: WombatEnvironmentmanager: RepairManagerwindfarm: Windfarmsettings: PortConfigturbine_manager: simpy.Resourcecrew_manager: simpy.Resourcetugboat_manager: simpy.Resourceactive_repairs: dict[str, dict[str, simpy.events.Event]]wait_until_next_shift()proces_repair(request: RepairRequest, time_processed: float, prior_operational_level: float)repair_single(request: RepairRequest)transfer_requests_from_manager(system_id: str)run_repairs(system_id: str)run_tow_to_port(request: RepairRequest)run_unscheduled_in_situ(request: RepairRequest)ServiceEquipmentenv: WombatEnvironmentmanager: RepairManagerwindfarm: Windfarmsettings: ScheduledServiceEquipmentData | UnscheduledServiceEquipmentDataat_port: boolat_system: boolonsite: boolenroute: booltransferring_crew: boolcurrent_system: boolcalculate_equipment_cost(duration: float)calculate_hourly_cost(duration: float)calculate_salary_cost(duration: float)find_interrupted_weather_window(hours_required: float)find_uninterrupted_weather_window(hours_required: float)run_scheduled()run_unscheduled()mobilize()mobilize_scheduled()weather_delay(hours: float)wait_until_next_shift()wait_until_next_operational_period()proces_repair(request: RepairRequest, time_processed: float, prior_operational_level: float)crew_transfer(system: System, subassembly: Subassembly, requests: RepairRequest, to_system: bool)repair(hours: float, request_details: Failure | Maintenance)travel(start: str, end: str, set_current: str)Windfarmenv: WombatEnvironmentwindfarm_layout: strrepair_manager: RepairManagercapacity: floatgraph: networkx.DiGraphsystem_list: list[str]substation_id: list[str]turbine_id: list[str]distance_matrix: numpy.ndarraycurrent_availability: floatcurrent_availability_wo_servicing: floatcalculate_distance_matrix()_log_operations()system(system_id: str) : Systemcable(cable_id: str | tuple) : CableSystemenv: WombatEnvironmentid: strname: stroperating_level: floatoperating_level_wo_servicing: floatpower_curve: Callablerepair_manager: RepairManagerservicing: boolvalue: floatcable_failure: boolcapacity: floatsubassemblies: list[Subassembly]electrical_system: Subasemblyelectronic_control: Subasemblygearbox: Subasemblygenerator: Subasemblyhydraulic_system: Subasemblymechanical_brake: Subasemblyrotor_blades: Subasemblyrotor_hub: Subasemblysensors: Subasemblysupporting_structure: Subasemblyyaw_system: Subasemblytransformer: Subasemblypower(windspeed: float | numpy.ndaray)interrupt_all_subassembly_processes()Subassemblyid: strname: strenv: WombatEnvironmentdata: SubassemblyDatabroken: booloperating_level: floatturbine: Systemprocesses: dictrun_single_failure(failure: Failure)run_single_maintenance(maintenance: Maintenance)interrupt_processes()interrupt_all_subassembly_processes()Cableid: strname: strenv: WombatEnvironmentdata: SubassemblyDatabroken: boolservicing: booldownstream_failure: boolprocesses: dictoperating_level: floatoperating_level_wo_servicing: floatrepair_manager: RepairManagerservicing: boolvalue: floatcable_failure: boolcapacity: floatrun_single_failure(failure: Failure)run_single_maintenance(maintenance: Maintenance)interrupt_processes()interrupt_all_subassembly_processes()stop_all_upstream_processes()RepairManagerenv: WombatEnvironmentwindfarm: Windfarmdowntime_based_equipment: StrategyMaprequest_based_equipment: StrategyMaprequest_map: dict[str, int]submit_request(request: RepairRequest) : RepairREquestpurge_subassembly_requests(system_id: str, subassembly_id: str, exclude: list[str])get_next_highest_severity_request(equipment_capability: list[str], severity_level: int)get_request_by_system(equipment_capability: list[str], system_id: str)StrategyMapCTV: list[EquipmentMap]LCN: list[EquipmentMap]SCN: list[EquipmentMap]CAB: list[EquipmentMap]DSV: list[EquipmentMap]DRN: list[EquipmentMap]RMT: list[EquipmentMap]is_running: boolupdate(capability: str, threshold: float | int, equipment: ServiceEquipment)EquipmentMapequipment_map: ServiceEquipmentstrategy_threshold: int | floatWombatEnvironmentdata_dir: pathlib.Pathweather_file: strworkday_end: intworkday_end: intsimulation_name: strstart_year: intend_year: intweather: pandas.DataFramemax_run_time: intshift_length: intweather_now: tuple[float, float]simulation_time: datetime.datetimecurrent_time: datetime.datetimeevents_log_fname: stroperations_log_fname: strpower_potential_fname: strpower_production_fname: strrun(until: int | float)hour_in_shift(hour: int, workday_start: int, workday_end: int) : boolhours_to_next_shift(workday_start: int) : floatdate_ix(date: datetime.datetime) int)weather_forecast(hours: int | float)convert_logs_to_csv(delete_original: bool, return_df: True)power_production_potential_to_csv(windfarm: Windfarm, operations: pandas.DataFrame, return_df: bool)cleanup_log_files(log_only: bool)Simulationenv: WombatEnvironmentlibrary_path: pathlib.Pathconfig: dict | Configurationwindfarm: Windfarmrepair_manager: RepairManagerservice_equipment: list[ServiceEquipment]metrics: Metricsfrom_config(cls, config: pathlib.Path | dict)run(until: int, create_metrics: bool, save_metrics_inputs: bool)save_metrics_inputs()Configurationname: strlibrary_path: pathlib.Pathlayout: strservice_equipment: str | list[str]weather: str | pandas.DataFrameworkday_start: intworkday_end: intinflation_rate: floatfixed_costs: strproject_capacity: floatstart_year: intend_year: intSAM_settings: strMetricsdata_dir: pathlib.Pathevents: str | pandas.DataFrameoperations: str | pandas.DataFramepotential: str | pandas.DataFrameproduction: str | pandas.DataFrameinflation_rate: floatproject_capacity: floatturbine_capacities: list[float]substation_id: list[str]turbine_id: list[str]service_equipment_names: list[str]fixed_costs: strSAM_settings: strfrom_simulation_outputs(cls, fpath: Path | str, fname: str)time_based_availability(frequency: str, by: str)production_based_availability(frequency: str, by: str)capacity_factor(which: str, frequency: str, by: str)task_completion_rate(which: str, frequency: str)equipment_costs(frequency: str, by_equipment: bool)service_equipment_utilization(frequency: str)labor_costs(frequency: str, by_type: bool)equipment_labor_cost_breakdowns(frequency: str, by_category: bool)component_costs(frequency: str, by_category: bool, by_action: bool)project_fixed_costs(frequency: str, resolution: str)process_times()power_production(frequency: str, by_turbine: bool)pysam_npv()pysam_lcoe_real()pysam_lcoe_nominal()pysam_irr()pysam_all_outputs() diff --git a/_images/data_classes.svg b/_images/data_classes.svg new file mode 100644 index 00000000..417c1fb6 --- /dev/null +++ b/_images/data_classes.svg @@ -0,0 +1 @@ +FromDictMixinfrom_dict(cls, data)Failurelevel: intscale: floatshape: floattime: floatmaterials: floatdescription: stroperation_reduction: floatservice_equipment: list[str]system_value: floatweibullrequest_id: strassign_id(request_id: str)hours_to_next_failure() : floatMaintenancetime: floatmaterials: floatdescription: stroperation_reduction: floatservice_equipment: list[str]system_value: floatrequest_id: strassign_id(request_id: str)RepairRequestcable: booldetails: Failure | Maintenancerequest_id: strseverity_level: intsubassembly_id: strsubassembly_name: strsystem_id: strsystem_name: strupstream_turbines: list[str]assign_id(request_id: str)ServiceEquipmentDatadata_dict: dictstrategy: strdetermine_type()ServiceCrewday_rate: floathourly_rate: floatn_day_rate: intn_hourly_rate: intScheduledServiceEquipmentDataname: strcapability: list[str]equipment_rate: floatcrew: ServiceCrewn_crews: intcrew_transfer_time: floatstart_day: intstart_month: intend_day: intend_month: intstart_year: intend_year: intstrategy: stronsite: boolworkday_start: intworkday_end: intmethod: strmobilization_cost: floatmobilization_days: intspeed: floatmax_waveheight_repair: floatmax_waveheight_transport: floatmax_windspeed_repair: floatmax_windspeed_transport: floatoperating_dates: numpy.ndarraycreate_date_range()UnscheduledServiceEquipmentDataname: strcapability: list[str]equipment_rate: floatcrew: ServiceCrewn_crews: intcrew_transfer_time: floatcharter_days: intstrategy: strstrategy_threshold: int | floatworkday_start: intworkday_end: intmethod: strmobilization_cost: floatmobilization_days: intspeed: floattow_sped: floatspeed_reduction_factor: floatmax_waveheight_repair: floatmax_waveheight_transport: floatmax_windspeed_repair: floatmax_windspeed_transport: floatport_distance: floatunmoor_hours: floatreconnection_hours: floatonsite: boolPortConfigname: strtugboats: list[UnscheduledServiceEquipmentData]crew: ServiceCrewn_crews: intmax_operations: intworkday_start: intworkday_end: intsite_distance: floatannual_fee: floatSubassemblyDataname: strsystem_value: floatfailures: dict[int, Failure]maintenance: list[Maintenance]FixedCostsoperations: floatannual_lease_fees: floatbrokers_fee: floatbusiness_interruption: floatcondition_monitoring: floatenvironmental_health_safety_monitoring: floatinsurance: floatlabor: floatmarine_management: floatonshore_electrical_maintenance: floatoperating_facilities: floatoperations_all_risk: floatoperations_management_administration: floatproject_management_administration: floatstorm_coverage: floatsubmerge_land_lease_costs: floatthird_party_liability: floattransmission_charges_rights: floatweather_forecasting: floathierarchy: dictresolution: dictcost_category_validator(name: str, sub_name: list[str])ServiceEquipmentenv: WombatEnvironmentmanager: RepairManagerwindfarm: Windfarmsettings: ScheduledServiceEquipmentData | UnscheduledServiceEquipmentDataat_port: boolat_system: boolonsite: boolenroute: booltransferring_crew: boolcurrent_system: boolcalculate_equipment_cost(duration: float)calculate_hourly_cost(duration: float)calculate_salary_cost(duration: float)find_interrupted_weather_window(hours_required: float)find_uninterrupted_weather_window(hours_required: float)run_scheduled()run_unscheduled()mobilize()mobilize_scheduled()weather_delay(hours: float)wait_until_next_shift()wait_until_next_operational_period()proces_repair(request: RepairRequest, time_processed: float, prior_operational_level: float)crew_transfer(system: System, subassembly: Subassembly, requests: RepairRequest, to_system: bool)repair(hours: float, request_details: Failure | Maintenance)travel(start: str, end: str, set_current: str)Systemenv: WombatEnvironmentid: strname: stroperating_level: floatoperating_level_wo_servicing: floatpower_curve: Callablerepair_manager: RepairManagerservicing: boolvalue: floatcable_failure: boolcapacity: floatsubassemblies: list[Subassembly]electrical_system: Subasemblyelectronic_control: Subasemblygearbox: Subasemblygenerator: Subasemblyhydraulic_system: Subasemblymechanical_brake: Subasemblyrotor_blades: Subasemblyrotor_hub: Subasemblysensors: Subasemblysupporting_structure: Subasemblyyaw_system: Subasemblytransformer: Subasemblypower(windspeed: float | numpy.ndaray)interrupt_all_subassembly_processes()Subassemblyid: strname: strenv: WombatEnvironmentdata: SubassemblyDatabroken: booloperating_level: floatturbine: Systemprocesses: dictrun_single_failure(failure: Failure)run_single_maintenance(maintenance: Maintenance)interrupt_processes()interrupt_all_subassembly_processes()Cableid: strname: strenv: WombatEnvironmentdata: SubassemblyDatabroken: boolservicing: booldownstream_failure: boolprocesses: dictoperating_level: floatoperating_level_wo_servicing: floatrepair_manager: RepairManagerservicing: boolvalue: floatcable_failure: boolcapacity: floatrun_single_failure(failure: Failure)run_single_maintenance(maintenance: Maintenance)interrupt_processes()interrupt_all_subassembly_processes()stop_all_upstream_processes()Portenv: WombatEnvironmentmanager: RepairManagerwindfarm: Windfarmsettings: PortConfigturbine_manager: simpy.Resourcecrew_manager: simpy.Resourcetugboat_manager: simpy.Resourceactive_repairs: dict[str, dict[str, simpy.events.Event]]wait_until_next_shift()proces_repair(request: RepairRequest, time_processed: float, prior_operational_level: float)repair_single(request: RepairRequest)transfer_requests_from_manager(system_id: str)run_repairs(system_id: str)run_tow_to_port(request: RepairRequest)run_unscheduled_in_situ(request: RepairRequest) diff --git a/_images/high_level_diagram.svg b/_images/high_level_diagram.svg new file mode 100644 index 00000000..ef1ed7f4 --- /dev/null +++ b/_images/high_level_diagram.svg @@ -0,0 +1 @@ +Wind Farm ModelTurbinesSubstationsRepair ManagerServicing EquipmentCore ModelCablesElectrical SystemElectronic ControlRotor BladesDrivetrainGearboxGeneratorSensorsMechanical BrakeHydraulic SystemSupporting StructureRotor HubYaw SystemTransformerCableSystemsExample SubassembliesEnvironmentPortDataClassesSimulation APIPost Processing diff --git a/_images/package_hierarchy.svg b/_images/package_hierarchy.svg new file mode 100644 index 00000000..04a54bf8 --- /dev/null +++ b/_images/package_hierarchy.svg @@ -0,0 +1 @@ +
wombat.core
wombat
wombat.windfarm
wombat.utilities
simulation_api
post_processor
data_classes
library
environment
repair_management
service_equipment
windfarm
wombat.windfarm.system
system
subassembly
cable
utilities
diff --git a/_images/simulation_api.svg b/_images/simulation_api.svg new file mode 100644 index 00000000..43ded337 --- /dev/null +++ b/_images/simulation_api.svg @@ -0,0 +1 @@ +WombatEnvironmentdata_dir: pathlib.Pathweather_file: strworkday_end: intworkday_end: intsimulation_name: strstart_year: intend_year: intweather: pandas.DataFramemax_run_time: intshift_length: intweather_now: tuple[float, float]simulation_time: datetime.datetimecurrent_time: datetime.datetimeevents_log_fname: stroperations_log_fname: strpower_potential_fname: strpower_production_fname: strrun(until: int | float)hour_in_shift(hour: int, workday_start: int, workday_end: int) : boolhours_to_next_shift(workday_start: int) : floatdate_ix(date: datetime.datetime) int)weather_forecast(hours: int | float)convert_logs_to_csv(delete_original: bool, return_df: True)power_production_potential_to_csv(windfarm: Windfarm, operations: pandas.DataFrame, return_df: bool)cleanup_log_files(log_only: bool)Simulationenv: WombatEnvironmentlibrary_path: pathlib.Pathconfig: dict | Configurationwindfarm: Windfarmrepair_manager: RepairManagerservice_equipment: list[ServiceEquipment]metrics: Metricsfrom_config(cls, config: pathlib.Path | dict)run(until: int, create_metrics: bool, save_metrics_inputs: bool)save_metrics_inputs()Configurationname: strlibrary_path: pathlib.Pathlayout: strservice_equipment: str | list[str]weather: str | pandas.DataFrameworkday_start: intworkday_end: intinflation_rate: floatfixed_costs: strproject_capacity: floatstart_year: intend_year: intSAM_settings: strMetricsdata_dir: pathlib.Pathevents: str | pandas.DataFrameoperations: str | pandas.DataFramepotential: str | pandas.DataFrameproduction: str | pandas.DataFrameinflation_rate: floatproject_capacity: floatturbine_capacities: list[float]substation_id: list[str]turbine_id: list[str]service_equipment_names: list[str]fixed_costs: strSAM_settings: strfrom_simulation_outputs(cls, fpath: Path | str, fname: str)time_based_availability(frequency: str, by: str)production_based_availability(frequency: str, by: str)capacity_factor(which: str, frequency: str, by: str)task_completion_rate(which: str, frequency: str)equipment_costs(frequency: str, by_equipment: bool)service_equipment_utilization(frequency: str)labor_costs(frequency: str, by_type: bool)equipment_labor_cost_breakdowns(frequency: str, by_category: bool)component_costs(frequency: str, by_category: bool, by_action: bool)project_fixed_costs(frequency: str, resolution: str)process_times()power_production(frequency: str, by_turbine: bool)pysam_npv()pysam_lcoe_real()pysam_lcoe_nominal()pysam_irr()pysam_all_outputs() diff --git a/_images/simulation_diagram.svg b/_images/simulation_diagram.svg new file mode 100644 index 00000000..e7e7351c --- /dev/null +++ b/_images/simulation_diagram.svg @@ -0,0 +1 @@ +Start simulationEnd simulationWind (and wave) time seriesSubassemblyFails or reaches maintenance intervalSystemRequests serviceRepair ManagerAssigns taskServicing EquipmentConducts serviceSystemReturns to operationAccumulate downtime (failures only)Track O&M costsWait for weather, equipment, parts*SubassemblyResets status diff --git a/_images/simulation_tools.svg b/_images/simulation_tools.svg new file mode 100644 index 00000000..8fb7e942 --- /dev/null +++ b/_images/simulation_tools.svg @@ -0,0 +1 @@ +ServiceEquipmentenv: WombatEnvironmentmanager: RepairManagerwindfarm: Windfarmsettings: ScheduledServiceEquipmentData | UnscheduledServiceEquipmentDataat_port: boolat_system: boolonsite: boolenroute: booltransferring_crew: boolcurrent_system: boolcalculate_equipment_cost(duration: float)calculate_hourly_cost(duration: float)calculate_salary_cost(duration: float)find_interrupted_weather_window(hours_required: float)find_uninterrupted_weather_window(hours_required: float)run_scheduled()run_unscheduled()mobilize()mobilize_scheduled()weather_delay(hours: float)wait_until_next_shift()wait_until_next_operational_period()proces_repair(request: RepairRequest, time_processed: float, prior_operational_level: float)crew_transfer(system: System, subassembly: Subassembly, requests: RepairRequest, to_system: bool)repair(hours: float, request_details: Failure | Maintenance)travel(start: str, end: str, set_current: str)Windfarmenv: WombatEnvironmentwindfarm_layout: strrepair_manager: RepairManagercapacity: floatgraph: networkx.DiGraphsystem_list: list[str]substation_id: list[str]turbine_id: list[str]distance_matrix: numpy.ndarraycurrent_availability: floatcurrent_availability_wo_servicing: floatcalculate_distance_matrix()_log_operations()system(system_id: str) : Systemcable(cable_id: str | tuple) : CableSystemenv: WombatEnvironmentid: strname: stroperating_level: floatoperating_level_wo_servicing: floatpower_curve: Callablerepair_manager: RepairManagerservicing: boolvalue: floatcable_failure: boolcapacity: floatsubassemblies: list[Subassembly]electrical_system: Subasemblyelectronic_control: Subasemblygearbox: Subasemblygenerator: Subasemblyhydraulic_system: Subasemblymechanical_brake: Subasemblyrotor_blades: Subasemblyrotor_hub: Subasemblysensors: Subasemblysupporting_structure: Subasemblyyaw_system: Subasemblytransformer: Subasemblypower(windspeed: float | numpy.ndaray)interrupt_all_subassembly_processes()Subassemblyid: strname: strenv: WombatEnvironmentdata: SubassemblyDatabroken: booloperating_level: floatturbine: Systemprocesses: dictrun_single_failure(failure: Failure)run_single_maintenance(maintenance: Maintenance)interrupt_processes()interrupt_all_subassembly_processes()Cableid: strname: strenv: WombatEnvironmentdata: SubassemblyDatabroken: boolservicing: booldownstream_failure: boolprocesses: dictoperating_level: floatoperating_level_wo_servicing: floatrepair_manager: RepairManagerservicing: boolvalue: floatcable_failure: boolcapacity: floatrun_single_failure(failure: Failure)run_single_maintenance(maintenance: Maintenance)interrupt_processes()interrupt_all_subassembly_processes()stop_all_upstream_processes()RepairManagerenv: WombatEnvironmentwindfarm: Windfarmdowntime_based_equipment: StrategyMaprequest_based_equipment: StrategyMaprequest_map: dict[str, int]submit_request(request: RepairRequest) : RepairREquestpurge_subassembly_requests(system_id: str, subassembly_id: str, exclude: list[str])get_next_highest_severity_request(equipment_capability: list[str], severity_level: int)get_request_by_system(equipment_capability: list[str], system_id: str)RepairRequestcable: booldetails: Failure | Maintenancerequest_id: strseverity_level: intsubassembly_id: strsubassembly_name: strsystem_id: strsystem_name: strupstream_turbines: list[str]assign_id(request_id: str)StrategyMapCTV: list[EquipmentMap]LCN: list[EquipmentMap]SCN: list[EquipmentMap]CAB: list[EquipmentMap]DSV: list[EquipmentMap]DRN: list[EquipmentMap]RMT: list[EquipmentMap]is_running: boolupdate(capability: str, threshold: float | int, equipment: ServiceEquipment)EquipmentMapequipment_map: ServiceEquipmentstrategy_threshold: int | floatWombatEnvironmentdata_dir: pathlib.Pathweather_file: strworkday_end: intworkday_end: intsimulation_name: strstart_year: intend_year: intweather: pandas.DataFramemax_run_time: intshift_length: intweather_now: tuple[float, float]simulation_time: datetime.datetimecurrent_time: datetime.datetimeevents_log_fname: stroperations_log_fname: strpower_potential_fname: strpower_production_fname: strrun(until: int | float)hour_in_shift(hour: int, workday_start: int, workday_end: int) : boolhours_to_next_shift(workday_start: int) : floatdate_ix(date: datetime.datetime) int)weather_forecast(hours: int | float)convert_logs_to_csv(delete_original: bool, return_df: True)power_production_potential_to_csv(windfarm: Windfarm, operations: pandas.DataFrame, return_df: bool)cleanup_log_files(log_only: bool)Simulationenv: WombatEnvironmentlibrary_path: pathlib.Pathconfig: dict | Configurationwindfarm: Windfarmrepair_manager: RepairManagerservice_equipment: list[ServiceEquipment]metrics: Metricsfrom_config(cls, config: pathlib.Path | dict)run(until: int, create_metrics: bool, save_metrics_inputs: bool)save_metrics_inputs()Configurationname: strlibrary_path: pathlib.Pathlayout: strservice_equipment: str | list[str]weather: str | pandas.DataFrameworkday_start: intworkday_end: intinflation_rate: floatfixed_costs: strproject_capacity: floatstart_year: intend_year: intSAM_settings: strMetricsdata_dir: pathlib.Pathevents: str | pandas.DataFrameoperations: str | pandas.DataFramepotential: str | pandas.DataFrameproduction: str | pandas.DataFrameinflation_rate: floatproject_capacity: floatturbine_capacities: list[float]substation_id: list[str]turbine_id: list[str]service_equipment_names: list[str]fixed_costs: strSAM_settings: strfrom_simulation_outputs(cls, fpath: Path | str, fname: str)time_based_availability(frequency: str, by: str)production_based_availability(frequency: str, by: str)capacity_factor(which: str, frequency: str, by: str)task_completion_rate(which: str, frequency: str)equipment_costs(frequency: str, by_equipment: bool)service_equipment_utilization(frequency: str)labor_costs(frequency: str, by_type: bool)equipment_labor_cost_breakdowns(frequency: str, by_category: bool)component_costs(frequency: str, by_category: bool, by_action: bool)project_fixed_costs(frequency: str, resolution: str)process_times()power_production(frequency: str, by_turbine: bool)pysam_npv()pysam_lcoe_real()pysam_lcoe_nominal()pysam_irr()pysam_all_outputs() diff --git a/_modules/index.html b/_modules/index.html new file mode 100644 index 00000000..92d67d46 --- /dev/null +++ b/_modules/index.html @@ -0,0 +1,429 @@ + + + + + + + + + + Overview: module code — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + + + + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/wombat/core/data_classes.html b/_modules/wombat/core/data_classes.html new file mode 100644 index 00000000..5b2c1d43 --- /dev/null +++ b/_modules/wombat/core/data_classes.html @@ -0,0 +1,2615 @@ + + + + + + + + + + wombat.core.data_classes — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for wombat.core.data_classes

+"""Turbine and turbine component shared utilities."""
+
+
+from __future__ import annotations
+
+import datetime
+from math import fsum
+from typing import TYPE_CHECKING, Any, Callable
+from pathlib import Path
+from functools import partial, update_wrapper
+from collections.abc import Sequence
+
+import attr
+import attrs
+import numpy as np
+import pandas as pd
+from attrs import Factory, Attribute, field, define
+
+from wombat.utilities.time import HOURS_IN_DAY, HOURS_IN_YEAR, parse_date
+
+
+if TYPE_CHECKING:
+    from wombat.core import ServiceEquipment
+
+
+# Define the valid servicing equipment types
+VALID_EQUIPMENT = (
+    "CTV",  # Crew tranfer vessel or onsite vehicle
+    "SCN",  # Small crane
+    "LCN",  # Large crane
+    "CAB",  # Cabling equipment
+    "RMT",  # Remote reset or anything performed remotely
+    "DRN",  # Drone
+    "DSV",  # Diving support vessel
+    "TOW",  # Tugboat or support vessel for moving a turbine to a repair facility
+    "AHV",  # Anchor handling vessel, typically a tugboat, w/o trigger tow-to-port
+)
+
+# Define the valid unscheduled and valid strategies
+UNSCHEDULED_STRATEGIES = ("requests", "downtime")
+VALID_STRATEGIES = tuple(["scheduled"] + list(UNSCHEDULED_STRATEGIES))
+
+
+def convert_to_list(
+    value: Sequence | str | int | float,
+    manipulation: Callable | None = None,
+) -> list:
+    """Convert an unknown element that could be a list or single, non-sequence element
+    to a list of elements.
+
+    Parameters
+    ----------
+    value : Sequence | str | int | float
+        The unknown element to be converted to a list of element(s).
+    manipulation: Callable | None
+        A function to be performed upon the individual elements, by default None.
+
+    Returns
+    -------
+    list
+        The new list of elements.
+    """
+    if isinstance(value, (str, int, float)):
+        value = [value]
+    if manipulation is not None:
+        return [manipulation(el) for el in value]
+    return list(value)
+
+
+convert_to_list_upper = partial(convert_to_list, manipulation=str.upper)
+update_wrapper(convert_to_list_upper, convert_to_list)
+
+convert_to_list_lower = partial(convert_to_list, manipulation=str.lower)
+update_wrapper(convert_to_list_lower, convert_to_list)
+
+
+def clean_string_input(value: str | None) -> str | None:
+    """Convert a string to lower case and and removes leading and trailing white spaces.
+
+    Parameters
+    ----------
+    value: str
+        The user input string.
+
+    Returns
+    -------
+    str
+        value.lower().strip()
+    """
+    if value is None:
+        return value
+    return value.lower().strip()
+
+
+def annual_date_range(
+    start_day: int,
+    end_day: int,
+    start_month: int,
+    end_month: int,
+    start_year: int,
+    end_year: int,
+) -> np.ndarray:
+    """Create a series of date ranges across multiple years between the starting date
+    and ending date for each year from starting year to ending year. Will only work if
+    the end year is the same as the starting year or later, and the ending month, day
+    combinatoion is after the starting month, day combination.
+
+    Parameters
+    ----------
+    start_day : int
+        Starting day of the month.
+    end_day : int
+        Ending day of the month.
+    start_month : int
+        Starting month.
+    end_month : int
+        Ending month.
+    start_year : int
+        First year of the date range.
+    end_year : int
+        Last year of the date range.
+
+    Raises
+    ------
+    ValueError: Raised if the ending month, day combination occur after the starting
+        month, day combination.
+    ValueError: Raised if the ending year is prior to the starting year.
+
+    Yields
+    ------
+    np.ndarray
+        A ``numpy.ndarray`` of ``datetime.date`` objects.
+    """
+    # Check the year bounds
+    if end_year < start_year:
+        raise ValueError(
+            f"The end_year ({start_year}) is later than the start_year ({end_year})."
+        )
+
+    # Check the month, date combination bounds
+    start = datetime.datetime(2022, start_month, start_day)
+    end = datetime.datetime(2022, end_month, end_day)
+    if end < start:
+        raise ValueError(
+            f"The starting month/day combination: {start}, is after the ending: {end}."
+        )
+
+    # Create a list of arrays of date ranges for each year
+    start = datetime.datetime(1, start_month, start_day)
+    end = datetime.datetime(1, end_month, end_day)
+    date_ranges = [
+        pd.date_range(start.replace(year=year), end.replace(year=year)).date
+        for year in range(start_year, end_year + 1)
+    ]
+
+    # Return a 1-D array
+    return np.hstack(date_ranges)
+
+
+def annualized_date_range(
+    start_date: datetime.datetime,
+    start_year: int,
+    end_date: datetime.datetime,
+    end_year: int,
+) -> np.ndarray:
+    """Create an annualized list of dates based on the simulation years.
+
+    Parameters
+    ----------
+    start_date : str
+        The string start month and day in MM/DD or MM-DD format.
+    start_year : int
+        The starting year in YYYY format.
+    end_date : str
+        The string end month and day in MM/DD or MM-DD format.
+    end_year : int
+        The ending year in YYYY format.
+
+    Returns
+    -------
+    set[datetime.datetime]
+        The set of dates the servicing equipment is available for operations.
+    """
+    additional = 1 if end_date < start_date else 0
+    date_list = [
+        pd.date_range(
+            start_date.replace(year=year),
+            end_date.replace(year=year + additional),
+            freq="D",
+        ).date
+        for year in range(start_year - additional, end_year + 1)
+    ]
+    dates = np.hstack(date_list)
+    start = datetime.date(start_year, 1, 1)
+    end = datetime.date(end_year, 12, 31)
+    dates = dates[(dates >= start) & (dates <= end)]
+    return dates
+
+
+def convert_ratio_to_absolute(ratio: int | float, total: int | float) -> int | float:
+    """Convert a proportional cost to an absolute cost.
+
+    Parameters
+    ----------
+    ratio : int | float
+        The proportional materials cost for a ``Maintenance`` or ``Failure``.
+    total: int | float
+        The turbine's replacement cost.
+
+    Returns
+    -------
+    int | float
+        The absolute cost of the materials.
+    """
+    if ratio <= 1:
+        return ratio * total
+    return ratio
+
+
+def check_start_stop_dates(
+    instance: Any, attribute: attr.Attribute, value: datetime.datetime | None
+) -> None:
+    """Ensure the date range start and stop points are not the same."""
+    if attribute.name == "non_operational_end":
+        start_date = instance.non_operational_start
+        start_name = "non_operational_start"
+    else:
+        start_date = instance.reduced_speed_start
+        start_name = "reduced_speed_start"
+
+    if value is None:
+        if start_date is None:
+            return
+        raise ValueError(
+            "A starting date was provided, but no ending date was provided"
+            f" for `{attribute.name}`."
+        )
+
+    if start_date is None:
+        raise ValueError(
+            "An ending date was provided, but no starting date was provided"
+            f" for `{start_name}`."
+        )
+
+    if start_date == value:
+        raise ValueError(
+            f"Starting date (`{start_name}`={start_date} and ending date"
+            f" (`{attribute.name}`={value}) cannot be the same date."
+        )
+
+
+def valid_hour(
+    instance: Any,
+    attribute: Attribute,
+    value: int,  # pylint: disable=W0613
+) -> None:
+    """Validate that the input is a valid time or null value (-1).
+
+    Parameters
+    ----------
+    instance : Any
+        A class object.
+    attribute : Attribute
+        The attribute being validated.
+    value : int
+        A whole number, hour of the day or -1 (null), where 24 indicates end of the day.
+
+    Raises
+    ------
+    ValueError
+        Raised if ``value`` is not between -1 and 24, inclusive.
+    """
+    if value == -1:
+        pass
+    elif value < 0 or value > 24:
+        raise ValueError(f"Input {attribute.name} must be between 0 and 24, inclusive.")
+
+
+def valid_reduction(
+    instance,
+    attribute: Attribute,
+    value: int | float,  # pylint: disable=W0613
+) -> None:
+    """Check to see if the reduction factor is between 0 and 1, inclusive.
+
+    Parameters
+    ----------
+    value : int | float
+        The input value for `speed_reduction_factor`.
+    """
+    if value < 0 or value > 1:
+        raise ValueError(
+            f"Input for {attribute.name}'s `speed_reduction_factor` must be between"
+            " 0 and 1, inclusive."
+        )
+
+
+def greater_than_zero(
+    instance,  # pylint: disable=W0613
+    attribute: Attribute,  # pylint: disable=W0613
+    value: int | float,
+) -> None:
+    """Check if an input is greater than 0.
+
+    Parameters
+    ----------
+    instance : Any
+        The class containing the attribute to be checked.
+    attribute : Attribute
+        The attribute's properties.
+    value : int | float
+        The user-input value for the ``attribute``.
+
+    Raises
+    ------
+    ValueError
+        Raised if ``value`` is less than or equal to zero.
+    """
+    if value <= 0:
+        raise ValueError("Input must be greater than 0.")
+
+
+def greater_than_equal_to_zero(
+    instance,  # pylint: disable=W0613
+    attribute: Attribute,  # pylint: disable=W0613
+    value: int | float,
+) -> None:
+    """Check if an input is at least 0.
+
+    Parameters
+    ----------
+    instance : Any
+        The class containing the attribute to be checked.
+    attribute : Attribute
+        The attribute's properties.
+    value : int | float
+        The user-input value for the ``attribute``.
+
+    Raises
+    ------
+    ValueError
+        Raised if ``value`` is less than or equal to zero.
+    """
+    if value < 0:
+        raise ValueError("Input must be at least 0.")
+
+
+
+[docs] +@define +class FromDictMixin: + """A Mixin class to allow for kwargs overloading when a data class doesn't + have a specific parameter definied. This allows passing of larger dictionaries + to a data class without throwing an error. + + Raises + ------ + AttributeError + Raised if the required class inputs are not provided. + """ + +
+[docs] + @classmethod + def from_dict(cls, data: dict): + """Map a data dictionary to an `attrs`-defined class. + + TODO: Add an error to ensure that either none or all the parameters are passed + + Parameters + ---------- + data : dict + The data dictionary to be mapped. + + Returns + ------- + cls : Any + The `attrs`-defined class. + """ + if TYPE_CHECKING: + assert hasattr(cls, "__attrs_attrs__") + # Get all parameters from the input dictionary that map to the class init + kwargs = { + a.name: data[a.name] + for a in cls.__attrs_attrs__ + if a.name in data and a.init + } + + # Map the inputs that must be provided: + # 1) must be initialized + # 2) no default value defined + required_inputs = [ + a.name + for a in cls.__attrs_attrs__ + if a.init and isinstance(a.default, attr._make._Nothing) # type: ignore + ] + undefined = sorted(set(required_inputs) - set(kwargs)) + if undefined: + raise AttributeError( + f"The class defintion for {cls.__name__} is missing the following" + f" inputs: {undefined}" + ) + return cls(**kwargs)
+
+ + + +
+[docs] +@define(frozen=True, auto_attribs=True) +class Maintenance(FromDictMixin): + """Data class to store maintenance data used in subassembly and cable modeling. + + Parameters + ---------- + time : float + Amount of time required to perform maintenance, in hours. + materials : float + Cost of materials required to perform maintenance, in USD. + frequency : float + Optimal number of days between performing maintenance, in days. + service_equipment: list[str] | str + Any combination of th following ``Equipment.capability`` options. + + - RMT: remote (no actual equipment BUT no special implementation) + - DRN: drone + - CTV: crew transfer vessel/vehicle + - SCN: small crane (i.e., field support vessel) + - LCN: large crane (i.e., heavy lift vessel) + - CAB: cabling vessel/vehicle + - DSV: diving support vessel + - TOW: tugboat or towing equipment + - AHV: anchor handling vessel (tugboat that doesn't trigger tow-to-port) + system_value : Union[int, float] + Turbine replacement value. Used if the materials cost is a proportional cost. + description : str + A short text description to be used for logging. + operation_reduction : float + Performance reduction caused by the failure, between (0, 1]. Defaults to 0. + + .. warning:: As of v0.7, availability is very sensitive to the usage of this + parameter, and so it should be used carefully. + + level : int, optional + Severity level of the maintenance. Defaults to 0. + """ + + time: float = field(converter=float) + materials: float = field(converter=float) + frequency: float = field(converter=float) + service_equipment: list[str] = field( + converter=convert_to_list_upper, + validator=attrs.validators.deep_iterable( + member_validator=attrs.validators.in_(VALID_EQUIPMENT), + iterable_validator=attrs.validators.instance_of(list), + ), + ) + system_value: int | float = field(converter=float) + description: str = field(default="routine maintenance", converter=str) + operation_reduction: float = field(default=0.0, converter=float) + level: int = field(default=0, converter=int) + request_id: str = field(init=False) + replacement: bool = field(default=False, init=False) + + def __attrs_post_init__(self): + """Convert frequency to hours (simulation time scale) and the equipment + requirement to a list. + """ + object.__setattr__(self, "frequency", self.frequency * HOURS_IN_DAY) + object.__setattr__( + self, + "materials", + convert_ratio_to_absolute(self.materials, self.system_value), + ) + +
+[docs] + def assign_id(self, request_id: str) -> None: + """Assign a unique identifier to the request. + + Parameters + ---------- + request_id : str + The ``wombat.core.RepairManager`` generated identifier. + """ + object.__setattr__(self, "request_id", request_id)
+
+ + + +
+[docs] +@define(frozen=True, auto_attribs=True) +class Failure(FromDictMixin): + """Data class to store failure data used in subassembly and cable modeling. + + Parameters + ---------- + scale : float + Weibull scale parameter for a failure classification. + shape : float + Weibull shape parameter for a failure classification. + time : float + Amount of time required to complete the repair, in hours. + materials : float + Cost of the materials required to complete the repair, in $USD. + operation_reduction : float + Performance reduction caused by the failure, between (0, 1]. + + .. warning:: As of v0.7, availability is very sensitive to the usage of this + parameter, and so it should be used carefully. + + level : int, optional + Level of severity, will be generated in the ``ComponentData.create_severities`` + method. + service_equipment: list[str] | str + Any combination of the following ``Equipment.capability`` options: + + - RMT: remote (no actual equipment BUT no special implementation) + - DRN: drone + - CTV: crew transfer vessel/vehicle + - SCN: small crane (i.e., field support vessel) + - LCN: large crane (i.e., heavy lift vessel) + - CAB: cabling vessel/vehicle + - DSV: diving support vessel + - TOW: tugboat or towing equipment + - AHV: anchor handling vessel (tugboat that doesn't trigger tow-to-port) + system_value : Union[int, float] + Turbine replacement value. Used if the materials cost is a proportional cost. + replacement : bool + True if triggering the failure requires a subassembly replacement, False, if + only a repair is necessary. Defaults to False + description : str + A short text description to be used for logging. + rng : np.random._generator.Generator + + .. note:: Do not provide this, it comes from + :py:class:`wombat.core.environment.WombatEnvironment` + + The shared random generator used for the Weibull sampling. This is fed through + the simulation environment to ensure consistent seeding between simulations. + """ + + scale: float = field(converter=float) + shape: float = field(converter=float) + time: float = field(converter=float) + materials: float = field(converter=float) + operation_reduction: float = field(converter=float) + level: int = field(converter=int) + service_equipment: list[str] | str = field( + converter=convert_to_list_upper, + validator=attrs.validators.deep_iterable( + member_validator=attrs.validators.in_(VALID_EQUIPMENT), + iterable_validator=attrs.validators.instance_of(list), + ), + ) + system_value: int | float = field(converter=float) + rng: np.random._generator.Generator = field( + eq=False, + validator=attrs.validators.instance_of(np.random._generator.Generator), + ) + replacement: bool = field(default=False, converter=bool) + description: str = field(default="failure", converter=str) + request_id: str = field(init=False) + + def __attrs_post_init__(self): + """Create the actual Weibull distribution and converts equipment requirements + to a list. + """ + object.__setattr__( + self, + "service_equipment", + convert_to_list(self.service_equipment, str.upper), + ) + object.__setattr__( + self, + "materials", + convert_ratio_to_absolute(self.materials, self.system_value), + ) + +
+[docs] + def hours_to_next_failure(self) -> float | None: + """Sample the next time to failure in a Weibull distribution. If the ``scale`` + and ``shape`` parameters are set to 0, then the model will return ``None`` to + cause the subassembly to timeout to the end of the simulation. + + Returns + ------- + float | None + Returns ``None`` for a non-modelled failure, or the time until the next + simulated failure. + """ + if self.scale == self.shape == 0: + return None + + return self.rng.weibull(self.shape, size=1)[0] * self.scale * HOURS_IN_YEAR
+ + +
+[docs] + def assign_id(self, request_id: str) -> None: + """Assign a unique identifier to the request. + + Parameters + ---------- + request_id : str + The ``wombat.core.RepairManager`` generated identifier. + """ + object.__setattr__(self, "request_id", request_id)
+
+ + + +
+[docs] +@define(frozen=True, auto_attribs=True) +class SubassemblyData(FromDictMixin): + """Data storage and validation class for the subassemblies. + + Parameters + ---------- + name : str + Name of the component/subassembly. + maintenance : list[dict[str, float | str]] + List of the maintenance classification dictionaries. This will be converted + to a list of ``Maintenance`` objects in the post initialization hook. + failures : dict[int, dict[str, float | str]] + Dictionary of failure classifications in a numerical (ordinal) categorization + order. This will be converted to a dictionary of ``Failure`` objects in the + post initialization hook. + system_value : int | float + Turbine's cost of replacement. Used in case percentages of turbine cost are used + in place of an absolute cost. + """ + + name: str = field(converter=str) + maintenance: list[Maintenance | dict[str, float | str]] + failures: list[Failure | dict[str, float | str]] | dict[ + int, Failure | dict[str, float | str] + ] + system_value: int | float = field(converter=float) + rng: np.random._generator.Generator = field( + validator=attrs.validators.instance_of(np.random._generator.Generator) + ) + + def __attrs_post_init__(self): + """Convert the maintenance and failure data to ``Maintenance`` and ``Failure`` + objects, respectively. + """ + for kwargs in self.maintenance: + if TYPE_CHECKING: + assert isinstance(kwargs, dict) + kwargs.update({"system_value": self.system_value}) + object.__setattr__( + self, + "maintenance", + [ + Maintenance.from_dict(kw) if isinstance(kw, dict) else kw + for kw in self.maintenance + ], + ) + + for kwargs in self.failures.values(): # type: ignore + if TYPE_CHECKING: + assert isinstance(kwargs, dict) + kwargs.update({"system_value": self.system_value}) + + failures_list = [] + rng_dict = {"rng": self.rng} + if TYPE_CHECKING: + assert isinstance(self.failures, dict) + for config in self.failures.values(): + if TYPE_CHECKING: + assert isinstance(config, dict) + config.update(rng_dict) + failures_list.append(Failure.from_dict(config)) + object.__setattr__(self, "failures", failures_list)
+ + + +
+[docs] +@define(frozen=True, auto_attribs=True) +class RepairRequest(FromDictMixin): + """Repair/Maintenance request data class. + + Parameters + ---------- + system_id : str + ``System.id``. + system_name : str + ``System.name``. + subassembly_id : str + ``Subassembly.id``. + subassembly_name : str + ``Subassembly.name``. + severity_level : int + ``Maintenance.level`` or ``Failure.level``. + details : Failure | Maintenance + The actual data class. + cable : bool + Indicator that the request is for a cable, by default False. + upstream_turbines : list[str] + The cable's upstream turbines, by default []. No need to use this if + ``cable`` == False. + upstream_cables : list[str] + The cable's upstream cables, by default []. No need to use this if + ``cable`` == False. + """ + + system_id: str = field(converter=str) + system_name: str = field(converter=str) + subassembly_id: str = field(converter=str) + subassembly_name: str = field(converter=str) + severity_level: int = field(converter=int) + details: Failure | Maintenance = field( + validator=attrs.validators.instance_of((Failure, Maintenance)) + ) + cable: bool = field(default=False, converter=bool, kw_only=True) + upstream_turbines: list[str] = field(default=Factory(list), kw_only=True) + upstream_cables: list[str] = field(default=Factory(list), kw_only=True) + request_id: str = field(init=False) + +
+[docs] + def assign_id(self, request_id: str) -> None: + """Assign a unique identifier to the request. + + Parameters + ---------- + request_id : str + The ``wombat.core.RepairManager`` generated identifier. + """ + object.__setattr__(self, "request_id", request_id) + self.details.assign_id(request_id)
+ + + def __eq__(self, other) -> bool: + """Redefines the equality method to only check for the ``request_id``.""" + return self.request_id == other.request_id
+ + + +
+[docs] +@define(frozen=True, auto_attribs=True) +class ServiceCrew(FromDictMixin): + """An internal data class for the indivdual crew units that are on the servicing + equipment. + + Parameters + ---------- + n_day_rate: int + Number of salaried workers. + day_rate: float + Day rate for salaried workers, in USD. + n_hourly_rate: int + Number of hourly/subcontractor workers. + hourly_rate: float + Hourly labor rate for subcontractors, in USD. + """ + + n_day_rate: int = field(converter=int) + day_rate: float = field(converter=float) + n_hourly_rate: int = field(converter=int) + hourly_rate: float = field(converter=float)
+ + + +class DateLimitsMixin: + """Base servicing equpment dataclass. Only meant to reduce repeated code across the + ``ScheduledServiceEquipmentData`` and ``UnscheduledServiceEquipmentData`` classes. + """ + + # MyPy type hints + port_distance: float = field(converter=float) + non_operational_start: datetime.datetime = field(converter=parse_date) + non_operational_end: datetime.datetime = field(converter=parse_date) + reduced_speed_start: datetime.datetime = field(converter=parse_date) + reduced_speed_end: datetime.datetime = field(converter=parse_date) + + def _set_environment_shift(self, start: int, end: int) -> None: + """Set the ``workday_start`` and ``workday_end`` to the environment's values. + + Parameters + ---------- + start : int + Starting hour of a workshift. + end : int + Ending hour of a workshift. + """ + object.__setattr__(self, "workday_start", start) + object.__setattr__(self, "workday_end", end) + if self.workday_start == 0 and self.workday_end == 24: # type: ignore + object.__setattr__(self, "non_stop_shift", True) + + def _set_port_distance(self, distance: int | float | None) -> None: + """Set ``port_distance`` from the environment's or port's variables. + + Parameters + ---------- + distance : int | float + The distance to port that must be traveled for servicing equipment. + """ + if distance is None: + return + if distance <= 0: + return + if self.port_distance <= 0: + object.__setattr__(self, "port_distance", float(distance)) + + def _compare_dates( + self, + new_start: datetime.datetime | None, + new_end: datetime.datetime | None, + which: str, + ) -> tuple[datetime.datetime | None, datetime.datetime | None]: + """Compare the orignal and newly input starting ane ending date for either the + non-operational date range or the reduced speed date range. + + Parameters + ---------- + new_start : datetime.datetime | None + The overriding start date if it is an earlier date than the original. + new_end : datetime.datetime | None + The overriding end date if it is a later date than the original. + which : str + One of "reduced_speed" or "non_operational" + + Returns + ------- + tuple[datetime.datetime | None, datetime.datetime | None] + The more conservative date pair between the new values and original values. + + Raises + ------ + ValueError + Raised if an invalid value to ``which`` is provided. + """ + if which in ("reduced_speed", "non_operational"): + original_start = getattr(self, f"{which}_start") + original_end = getattr(self, f"{which}_end") + else: + raise ValueError( + "`which` must be one of 'reduced_speed' or 'non_operational'." + ) + + if original_start is not None: + if new_start is not None: + new_start = min(new_start, original_start) + else: + new_start = original_start + else: + object.__setattr__(self, f"{which}_start", new_start) + + if original_end is not None: + if new_end is not None: + new_end = max(new_end, original_end) + else: + new_end = original_end + else: + object.__setattr__(self, f"{which}_end", new_end) + + return new_start, new_end + + def set_non_operational_dates( + self, + start_date: datetime.datetime | None = None, + start_year: int | None = None, + end_date: datetime.datetime | None = None, + end_year: int | None = None, + ) -> None: + """Create an annualized list of dates where servicing equipment should be + unavailable. Takes a a ``start_year`` and ``end_year`` to determine how many + years should be covered. + + Parameters + ---------- + start_date : datetime.datetime | None + The starting date for the annual date range of non-operational dates, by + default None. + start_year : int | None + The first year that has a non-operational date range, by default None. + end_date : datetime.datetime | None + The ending date for the annual range of non-operational dates, by default + None. + end_year : int | None + The last year that should have a non-operational date range, by default + None. + + Raises + ------ + ValueError + Raised if the starting and ending dates are the same date. + """ + start_date, end_date = self._compare_dates( + start_date, end_date, "non_operational" + ) + object.__setattr__(self, "non_operational_start", start_date) + object.__setattr__(self, "non_operational_end", end_date) + + # Check that the base dates are valid + if self.non_operational_start is None or self.non_operational_end is None: + object.__setattr__( + self, "non_operational_dates", np.empty(0, dtype="object") + ) + object.__setattr__(self, "non_operational_dates_set", set()) + return + + # Check that the input year range is valid + if not isinstance(start_year, int): + raise ValueError( + f"Input to `start_year`: {start_year}, must be an integer." + ) + if not isinstance(end_year, int): + raise ValueError(f"Input to `end_year`: {end_year}, must be an integer.") + if end_year < start_year: + raise ValueError( + "`start_year`: {start_year}, must less than or equal to the" + f" `end_year`: {end_year}" + ) + + # Create the date range + dates = annualized_date_range( + self.non_operational_start, start_year, self.non_operational_end, end_year + ) + object.__setattr__(self, "non_operational_dates", dates) + object.__setattr__(self, "non_operational_dates_set", set(dates)) + + # Update the operating dates field to ensure there is no overlap + if hasattr(self, "operating_dates") and hasattr(self, "non_operational_dates"): + operating = np.setdiff1d(self.operating_dates, self.non_operational_dates) + object.__setattr__(self, "operating_dates", operating) + object.__setattr__(self, "operating_dates_set", set(operating)) + + def set_reduced_speed_parameters( + self, + start_date: datetime.datetime | None = None, + start_year: int | None = None, + end_date: datetime.datetime | None = None, + end_year: int | None = None, + speed: float = 0.0, + ) -> None: + """Create an annualized list of dates where servicing equipment should be + operating with reduced speeds. The passed ``start_date``, ``end_date``, and + ``speed`` will override provided values if they are more conservative than the + current settings, or a value for ``speed`` will only override the existing value + if the passed value is slower. Takes a ``start_year`` and ``end_year`` to + determine how many years should be covered by this setting. + + Parameters + ---------- + start_date : datetime.datetime | None + The starting date for the annual date range of reduced speeds, by default + None. + start_year : int | None + The first year that has a reduced speed date range, by default None. + end_date : datetime.datetime | None + The ending date for the annual range of reduced speeds, by default None. + end_year : int | None + The last year that should have a reduced speed date range, by default None. + speed : float + The maximum operating speed under a speed-restricted scenario. + + Raises + ------ + ValueError + Raised if the starting and ending dates are the same date. + """ + # Check that the base dates are valid and override if no values were provided + # or if the new value is more conservative than the originally provided value + start_date, end_date = self._compare_dates( + start_date, end_date, "reduced_speed" + ) + object.__setattr__(self, "reduced_speed_start", start_date) + object.__setattr__(self, "reduced_speed_end", end_date) + + if start_date is None or end_date is None: + object.__setattr__(self, "reduced_speed_dates", np.empty(0, dtype="object")) + object.__setattr__(self, "reduced_speed_dates_set", set()) + return + + # Check that the input year range is valid + if not isinstance(start_year, int): + raise ValueError( + f"Input to `start_year`: {start_year}, must be an integer." + ) + if not isinstance(end_year, int): + raise ValueError(f"Input to `end_year`: {end_year}, must be an integer.") + if end_year < start_year: + raise ValueError( + "`start_year`: {start_year}, must less than or equal to the" + f" `end_year`: {end_year}" + ) + + # Create the date range + dates = annualized_date_range( + self.reduced_speed_start, start_year, self.reduced_speed_end, end_year + ) + object.__setattr__(self, "reduced_speed_dates", dates) + object.__setattr__(self, "reduced_speed_dates_set", set(dates)) + + # Update the reduced speed if none was originally provided + if TYPE_CHECKING: + assert hasattr(self, "reduced_speed") # mypy helper + if speed != 0 and (self.reduced_speed == 0 or speed < self.reduced_speed): + object.__setattr__(self, "reduced_speed", speed) + + +
+[docs] +@define(frozen=True, auto_attribs=True) +class ScheduledServiceEquipmentData(FromDictMixin, DateLimitsMixin): + """The data class specification for servicing equipment that will use a + pre-scheduled basis for returning to site. + + Parameters + ---------- + name: str + Name of the piece of servicing equipment. + equipment_rate: float + Day rate for the equipment/vessel, in USD. + n_crews : int + Number of crew units for the equipment. + + .. note:: The input to this does not matter yet, as multi-crew functionality + is not yet implemented. + + crew : ServiceCrew + The crew details, see :py:class:`ServiceCrew` for more information. Dictionary + of labor costs with the following: ``n_day_rate``, ``day_rate``, + ``n_hourly_rate``, and ``hourly_rate``. + start_month : int + The day to start operations for the rig and crew. + start_day : int + The month to start operations for the rig and crew. + start_year : int + The year to start operations for the rig and crew. + end_month : int + The month to end operations for the rig and crew. + end_day : int + The day to end operations for the rig and crew. + end_year : int + The year to end operations for the rig and crew. + + .. note:: if the rig comes annually, then the enter the year for the last year + that the rig and crew will be available. + + capability : str + The type of capabilities the equipment contains. Must be one of: + + - RMT: remote (no actual equipment BUT no special implementation) + - DRN: drone + - CTV: crew transfer vessel/vehicle + - SCN: small crane (i.e., field support vessel) + - LCN: large crane (i.e., heavy lift vessel) + - CAB: cabling vessel/vehicle + - DSV: diving support vessel + + Please note that "TOW" is unavailable for scheduled servicing equipment + mobilization_cost : float + Cost to mobilize the rig and crew. + mobilization_days : int + Number of days it takes to mobilize the equipment. + speed : float + Maximum transit speed, km/hr. + speed_reduction_factor : flaot + Reduction factor for traveling in inclement weather, default 0. When 0, travel + is stopped when either `max_windspeed_transport` or `max_waveheight_transport` + is reached, and when 1, `speed` is used. + max_windspeed_transport : float + Maximum windspeed for safe transport, m/s. + max_windspeed_repair : float + Maximum windspeed for safe operations, m/s. + max_waveheight_transport : float + Maximum waveheight for safe transport, m, default 1000 (land-based). + max_waveheight_repair : float + Maximum waveheight for safe operations, m, default 1000 (land-based). + workday_start : int + The starting hour of a workshift, in 24 hour time. + workday_end : int + The ending hour of a workshift, in 24 hour time. + crew_transfer_time : float + The number of hours it takes to transfer the crew from the equipment to the + system, e.g. how long does it take to transfer the crew from the CTV to the + turbine, default 0. + onsite : bool + Indicator for if the servicing equipment and crew are based onsite. + + .. note:: If based onsite, be sure that the start and end dates represent the + first and last day/month of the year, respectively, and the start and end + years represent the fist and last year in the weather file. + + method : str + Determines if the equipment will do all maximum severity repairs first or do all + the repairs at one turbine before going to the next, by default severity. Must + be one of "severity" or "turbine". + port_distance : int | float + The distance, in km, the equipment must travel to go between port and site, by + default 0. + non_operational_start : str | datetime.datetime | None + The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of prohibited operations. When defined at the environment level, an + undefined or later starting date will be overridden, by default None. + non_operational_end : str | datetime.datetime | None + The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of prohibited operations. When defined at the environment level, an + undefined or earlier ending date will be overridden, by default None. + reduced_speed_start : str | datetime.datetime | None + The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of reduced speed operations. When defined at the environment level, an + undefined or later starting date will be overridden, by default None. + reduced_speed_end : str | datetime.datetime | None + The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of reduced speed operations. When defined at the environment level, an + undefined or earlier ending date will be overridden, by default None. + reduced_speed : float + The maximum operating speed during the annualized reduced speed operations. + When defined at the environment level, an undefined or faster value will be + overridden, by default 0.0. + """ + + name: str = field(converter=str) + equipment_rate: float = field(converter=float) + n_crews: int = field(converter=int) + crew: ServiceCrew = field(converter=ServiceCrew.from_dict) + capability: list[str] = field( + converter=convert_to_list_upper, + validator=attrs.validators.deep_iterable( + member_validator=attrs.validators.in_(VALID_EQUIPMENT), + iterable_validator=attrs.validators.instance_of(list), + ), + ) + speed: float = field(converter=float, validator=greater_than_zero) + max_windspeed_transport: float = field(converter=float) + max_windspeed_repair: float = field(converter=float) + + mobilization_cost: float = field(default=0, converter=float) + mobilization_days: int = field(default=0, converter=int) + max_waveheight_transport: float = field(default=1000.0, converter=float) + max_waveheight_repair: float = field(default=1000.0, converter=float) + workday_start: int = field(default=-1, converter=int, validator=valid_hour) + workday_end: int = field(default=-1, converter=int, validator=valid_hour) + crew_transfer_time: float = field(converter=float, default=0.0) + speed_reduction_factor: float = field( + default=0.0, converter=float, validator=valid_reduction + ) + port_distance: float = field(default=0.0, converter=float) + onsite: bool = field(default=False, converter=bool) + method: str = field( + default="severity", + converter=[str, str.lower], + validator=attrs.validators.in_(["turbine", "severity"]), + ) + start_month: int = field( + default=-1, converter=int, validator=attrs.validators.ge(0) + ) + start_day: int = field(default=-1, converter=int, validator=attrs.validators.ge(0)) + start_year: int = field(default=-1, converter=int, validator=attrs.validators.ge(0)) + end_month: int = field(default=-1, converter=int, validator=attrs.validators.ge(0)) + end_day: int = field(default=-1, converter=int, validator=attrs.validators.ge(0)) + end_year: int = field(default=-1, converter=int, validator=attrs.validators.ge(0)) + strategy: str = field( + default="scheduled", validator=attrs.validators.in_(["scheduled"]) + ) + operating_dates: np.ndarray = field(factory=list, init=False) + operating_dates_set: set = field(factory=set, init=False) + + non_operational_start: datetime.datetime = field(default=None, converter=parse_date) + non_operational_end: datetime.datetime = field( + default=None, converter=parse_date, validator=check_start_stop_dates + ) + reduced_speed_start: datetime.datetime = field(default=None, converter=parse_date) + reduced_speed_end: datetime.datetime = field( + default=None, converter=parse_date, validator=check_start_stop_dates + ) + reduced_speed: float = field(default=0, converter=float) + non_operational_dates: pd.DatetimeIndex = field(factory=list, init=False) + non_operational_dates_set: pd.DatetimeIndex = field(factory=set, init=False) + reduced_speed_dates: pd.DatetimeIndex = field(factory=set, init=False) + non_stop_shift: bool = field(default=False, init=False) + + def create_date_range(self) -> np.ndarray: + """Create an ``np.ndarray`` of valid operational dates.""" + start_date = datetime.datetime( + self.start_year, self.start_month, self.start_day + ) + + # If `onsite` is True, then create the range in one go because there should be + # no discrepancies in the range. + # Othewise, create a specified date range between two dates in the year range. + if self.onsite: + end_date = datetime.datetime(self.end_year, self.end_month, self.end_day) + date_range = pd.date_range(start_date, end_date).date + else: + date_range = annual_date_range( + self.start_day, + self.end_day, + self.start_month, + self.end_month, + self.start_year, + self.end_year, + ) + return date_range + + def __attrs_post_init__(self) -> None: + """Post-initialization.""" + object.__setattr__(self, "operating_dates", self.create_date_range()) + object.__setattr__(self, "operating_dates_set", set(self.operating_dates)) + if self.workday_start == 0 and self.workday_end == 24: + object.__setattr__(self, "non_stop_shift", True)
+ + + +
+[docs] +@define(frozen=True, auto_attribs=True) +class UnscheduledServiceEquipmentData(FromDictMixin, DateLimitsMixin): + """The data class specification for servicing equipment that will use either a + basis of windfarm downtime or total number of requests serviceable by the equipment. + + Parameters + ---------- + name: str + Name of the piece of servicing equipment. + equipment_rate: float + Day rate for the equipment/vessel, in USD. + n_crews : int + Number of crew units for the equipment. + + .. note: the input to this does not matter yet, as multi-crew functionality + is not yet implemented. + + crew : ServiceCrew + The crew details, see :py:class:`ServiceCrew` for more information. Dictionary + of labor costs with the following: ``n_day_rate``, ``day_rate``, + ``n_hourly_rate``, and ``hourly_rate``. + charter_days : int + The number of days the servicing equipment can be chartered for. + capability : str + The type of capabilities the equipment contains. Must be one of: + - RMT: remote (no actual equipment BUT no special implementation) + - DRN: drone + - CTV: crew transfer vessel/vehicle + - SCN: small crane (i.e., field support vessel) + - LCN: large crane (i.e., heavy lift vessel) + - CAB: cabling vessel/vehicle + - DSV: diving support vessel + - TOW: tugboat or towing equipment + - AHV: anchor handling vessel (tugboat that doesn't trigger tow-to-port) + speed : float + Maximum transit speed, km/hr. + tow_speed : float + The maximum transit speed when towing, km/hr. + + .. note:: This is only required for when the servicing equipment is tugboat + enabled for a tow-to-port scenario (capability = "TOW") + + speed_reduction_factor : flaot + Reduction factor for traveling in inclement weather, default 0. When 0, travel + is stopped when either `max_windspeed_transport` or `max_waveheight_transport` + is reached, and when 1, `speed` is used. + max_windspeed_transport : float + Maximum windspeed for safe transport, m/s. + max_windspeed_repair : float + Maximum windspeed for safe operations, m/s. + max_waveheight_transport : float + Maximum waveheight for safe transport, m, default 1000 (land-based). + max_waveheight_repair : float + Maximum waveheight for safe operations, m, default 1000 (land-based). + mobilization_cost : float + Cost to mobilize the rig and crew, default 0. + mobilization_days : int + Number of days it takes to mobilize the equipment, default 0. + strategy : str + For any unscheduled maintenance servicing equipment, this determines the + strategy for dispatching. Should be on of "downtime" or "requests". + strategy_threshold : str + For downtime-based scenarios, this is based on the operating level, and should + be in the range (0, 1). For reqest-based scenarios, this is the maximum number + of requests that are allowed to build up for any given type of unscheduled + servicing equipment, should be an integer >= 1. + workday_start : int + The starting hour of a workshift, in 24 hour time. + workday_end : int + The ending hour of a workshift, in 24 hour time. + crew_transfer_time : float + The number of hours it takes to transfer the crew from the equipment to the + system, e.g. how long does it take to transfer the crew from the CTV to the + turbine, default 0. + onsite : bool + Indicator for if the rig and crew are based onsite. + + .. note:: if the rig and crew are onsite be sure that the start and end dates + represent the first and last day/month of the year, respectively, and the + start and end years represent the fist and last year in the weather file. + + method : str + Determines if the ship will do all maximum severity repairs first or do all + the repairs at one turbine before going to the next, by default severity. + Should by one of "severity" or "turbine". + unmoor_hours : int | float + The number of hours required to unmoor a floating offshore wind turbine in order + to tow it to port, by default 0. + + .. note:: Required for the tugboat/towing capability, otherwise unused. + + reconnection_hours : int | float + The number of hours required to reconnect a floating offshore wind turbine after + being towed back to site, by default 0. + + .. note:: Required for the tugboat/towing capability, otherwise unused. + + port_distance : int | float + The distance, in km, the equipment must travel to go between port and site, by + default 0. + non_operational_start : str | datetime.datetime | None + The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of prohibited operations. When defined at the environment level or the + port level, if a tugboat, an undefined or later starting date will be + overridden, by default None. + non_operational_end : str | datetime.datetime | None + The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of prohibited operations. When defined at the environment level or the + port level, if a tugboat, an undefined or earlier ending date will be + overridden, by default None. + reduced_speed_start : str | datetime.datetime | None + The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of reduced speed operations. When defined at the environment level or the + port level, if a tugboat, an undefined or later starting date will be + overridden, by default None. + reduced_speed_end : str | datetime.datetime | None + The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of reduced speed operations. When defined at the environment level or the + port level, if a tugboat, an undefined or earlier ending date will be + overridden, by default None. + reduced_speed : float + The maximum operating speed during the annualized reduced speed operations. + When defined at the environment level, an undefined or faster value will be + overridden, by default 0.0. + """ + + name: str = field(converter=str) + equipment_rate: float = field(converter=float) + n_crews: int = field(converter=int) + crew: ServiceCrew = field(converter=ServiceCrew.from_dict) + capability: list[str] = field( + converter=convert_to_list_upper, + validator=attrs.validators.deep_iterable( + member_validator=attrs.validators.in_(VALID_EQUIPMENT), + iterable_validator=attrs.validators.instance_of(list), + ), + ) + speed: float = field(converter=float, validator=greater_than_zero) + max_windspeed_transport: float = field(converter=float) + max_windspeed_repair: float = field(converter=float) + mobilization_cost: float = field(default=0, converter=float) + mobilization_days: int = field(default=0, converter=int) + max_waveheight_transport: float = field(default=1000.0, converter=float) + max_waveheight_repair: float = field(default=1000.0, converter=float) + workday_start: int = field(default=-1, converter=int, validator=valid_hour) + workday_end: int = field(default=-1, converter=int, validator=valid_hour) + crew_transfer_time: float = field(converter=float, default=0.0) + speed_reduction_factor: float = field( + default=0.0, converter=float, validator=valid_reduction + ) + port_distance: float = field(default=0.0, converter=float) + onsite: bool = field(default=False, converter=bool) + method: str = field( # type: ignore + default="severity", + converter=[str, str.lower], + validator=attrs.validators.in_(["turbine", "severity"]), + ) + strategy: str | None = field( + default="unscheduled", + converter=clean_string_input, + validator=attrs.validators.in_(UNSCHEDULED_STRATEGIES), + ) + strategy_threshold: int | float = field(default=-1, converter=float) + charter_days: int = field( + default=-1, converter=int, validator=attrs.validators.gt(0) + ) + tow_speed: float = field(default=1, converter=float, validator=greater_than_zero) + unmoor_hours: int | float = field(default=0, converter=float) + reconnection_hours: int | float = field(default=0, converter=float) + non_operational_start: datetime.datetime = field(default=None, converter=parse_date) + non_operational_end: datetime.datetime = field(default=None, converter=parse_date) + reduced_speed_start: datetime.datetime = field(default=None, converter=parse_date) + reduced_speed_end: datetime.datetime = field(default=None, converter=parse_date) + reduced_speed: float = field(default=0, converter=float) + non_operational_dates: pd.DatetimeIndex = field(factory=list, init=False) + non_operational_dates_set: pd.DatetimeIndex = field(factory=set, init=False) + reduced_speed_dates: pd.DatetimeIndex = field(factory=set, init=False) + non_stop_shift: bool = field(default=False, init=False) + +
+[docs] + @strategy_threshold.validator # type: ignore + def _validate_threshold( + self, + attribute: Attribute, # pylint: disable=W0613 + value: int | float, + ) -> None: + """Ensure a valid threshold is provided for a given ``strategy``.""" + if self.strategy == "downtime": + if value <= 0 or value >= 1: + raise ValueError( + "Downtime-based strategies must have a ``strategy_threshold``", + "between 0 and 1, non-inclusive!", + ) + if self.strategy == "requests": + if value <= 0: + raise ValueError( + "Requests-based strategies must have a ``strategy_threshold``", + "greater than 0!", + )
+ + + def __attrs_post_init__(self) -> None: + """Post-initialization hook.""" + if self.workday_start == 0 and self.workday_end == 24: + object.__setattr__(self, "non_stop_shift", True)
+ + + +
+[docs] +@define(frozen=True, auto_attribs=True) +class ServiceEquipmentData(FromDictMixin): + """Helps to determine the type ServiceEquipment that should be used, based on the + repair strategy for its operation. See + :py:class:`~data_classes.ScheduledServiceEquipmentData` or + :py:class:`~data_classes.UnscheduledServiceEquipmentData` for more details on each + classifcation. + + Parameters + ---------- + data_dict : dict + The dictionary that will be used to create the appropriate ServiceEquipmentData. + This should contain a field called 'strategy' with either "scheduled" or + "unscheduled" as a value if strategy is not provided as a keyword argument. + strategy : str, optional + Should be one of "scheduled", "requests", "downtime". If nothing is provided, + the equipment configuration will be checked. + + Raises + ------ + ValueError + Raised if ``strategy`` is not one of "scheduled" or "unscheduled". + + Examples + -------- + The below workflow is how a new data + :py:class:`~data_classes.ScheduledServiceEquipmentData` object could be created via + a generic/routinized creation method, and is how the + :py:class:`~service_equipment.ServiceEquipment`'s ``__init__`` method creates the + settings data. + + >>> from wombat.core.data_classes import ServiceEquipmentData + >>> + >>> data_dict = { + >>> "name": "Crew Transfer Vessel 1", + >>> "equipment_rate": 1750, + >>> "start_month": 1, + >>> "start_day": 1, + >>> "end_month": 12, + >>> "end_day": 31, + >>> "start_year": 2002, + >>> "end_year": 2014, + >>> "onsite": True, + >>> "capability": "CTV", + >>> "max_severity": 10, + >>> "mobilization_cost": 0, + >>> "mobilization_days": 0, + >>> "speed": 37.04, + >>> "max_windspeed_transport": 99, + >>> "max_windspeed_repair": 99, + >>> "max_waveheight_transport": 1.5, + >>> "max_waveheight_repair": 1.5, + >>> "strategy": scheduled, + >>> "crew_transfer_time": 0.25, + >>> "n_crews": 1, + >>> "crew": { + >>> "day_rate": 0, + >>> "n_day_rate": 0, + >>> "hourly_rate": 0, + >>> "n_hourly_rate": 0, + >>> }, + >>> } + >>> equipment = ServiceEquipmentData(data_dict).determine_type() + >>> type(equipment) + """ + + data_dict: dict + strategy: str | None = field( + kw_only=True, default=None, converter=clean_string_input + ) + + def __attrs_post_init__(self): + """Post-initialization.""" + if self.strategy is None: + object.__setattr__( + self, "strategy", clean_string_input(self.data_dict["strategy"]) + ) + if self.strategy not in VALID_STRATEGIES: + raise ValueError( + f"ServiceEquipment strategy should be one of {VALID_STRATEGIES};" + f" input: {self.strategy}." + ) + +
+[docs] + def determine_type( + self, + ) -> ScheduledServiceEquipmentData | UnscheduledServiceEquipmentData: + """Generate the appropriate ServiceEquipmentData variation. + + Returns + ------- + Union[ScheduledServiceEquipmentData, UnscheduledServiceEquipmentData] + The appropriate ``xxServiceEquipmentData`` schema depending on the strategy + the ``ServiceEquipment`` will use. + """ + if self.strategy == "scheduled": + return ScheduledServiceEquipmentData.from_dict(self.data_dict) + elif self.strategy in UNSCHEDULED_STRATEGIES: + return UnscheduledServiceEquipmentData.from_dict(self.data_dict) + else: + # This should not be able to be reached + raise ValueError("Invalid strategy provided!")
+
+ + + +@define(auto_attribs=True) +class EquipmentMap: + """Internal mapping for servicing equipment strategy information.""" + + strategy_threshold: int | float + equipment: ServiceEquipment # noqa: disable=F821 + + +@define(auto_attribs=True) +class StrategyMap: + """Internal mapping for equipment capabilities and their data.""" + + CTV: list[EquipmentMap] = field(factory=list) + SCN: list[EquipmentMap] = field(factory=list) + LCN: list[EquipmentMap] = field(factory=list) + CAB: list[EquipmentMap] = field(factory=list) + RMT: list[EquipmentMap] = field(factory=list) + DRN: list[EquipmentMap] = field(factory=list) + DSV: list[EquipmentMap] = field(factory=list) + TOW: list[EquipmentMap] = field(factory=list) + AHV: list[EquipmentMap] = field(factory=list) + is_running: bool = field(default=False, init=False) + + def update( + self, + capability: str, + threshold: int | float, + equipment: ServiceEquipment, # noqa: disable=F821 + ) -> None: + """Update the strategy mapping between capability types and the + available ``ServiceEquipment`` objects. + + Parameters + ---------- + capability : str + The ``equipment``'s capability. + threshold : int | float + The threshold for ``equipment``'s strategy. + equipment : ServiceEquipment + The actual ``ServiceEquipment`` object to be logged. + + Raises + ------ + ValueError + Raised if there is an invalid capability, though this shouldn't be able to + be reached. + """ + # Using a mypy ignore because of an unpatched bug using data classes + if capability == "CTV": + self.CTV.append(EquipmentMap(threshold, equipment)) # type: ignore + elif capability == "SCN": + self.SCN.append(EquipmentMap(threshold, equipment)) # type: ignore + elif capability == "LCN": + self.LCN.append(EquipmentMap(threshold, equipment)) # type: ignore + elif capability == "CAB": + self.CAB.append(EquipmentMap(threshold, equipment)) # type: ignore + elif capability == "RMT": + self.RMT.append(EquipmentMap(threshold, equipment)) # type: ignore + elif capability == "DRN": + self.DRN.append(EquipmentMap(threshold, equipment)) # type: ignore + elif capability == "DSV": + self.DSV.append(EquipmentMap(threshold, equipment)) # type: ignore + elif capability == "TOW": + self.TOW.append(EquipmentMap(threshold, equipment)) # type: ignore + elif capability == "AHV": + self.AHV.append(EquipmentMap(threshold, equipment)) # type: ignore + else: + # This should not even be able to be reached + raise ValueError( + f"Invalid servicing equipment '{capability}' has been provided!" + ) + self.is_running = True + + def get_mapping(self, capability) -> list[EquipmentMap]: + """Gets the attribute matching the desired :py:attr:`capability`. + + Parameters + ---------- + capability : str + A string matching one of the ``UnscheduledServiceEquipmentData.capability`` + (or scheduled) options. + + Returns + ------- + list[EquipmentMap] + Returns the matching mapping of available servicing equipment. + """ + if capability == "CTV": + return self.CTV + if capability == "SCN": + return self.SCN + if capability == "LCN": + return self.LCN + if capability == "CAB": + return self.CAB + if capability == "RMT": + return self.RMT + if capability == "DRN": + return self.DRN + if capability == "DSV": + return self.DSV + if capability == "TOW": + return self.TOW + if capability == "AHV": + return self.AHV + # This should not even be able to be reached + raise ValueError( + f"Invalid servicing equipmen capability '{capability}' has been provided!" + ) + + def move_equipment_to_end(self, capability: str, ix: int) -> None: + """Moves a used equipment to the end of the mapping list to ensure a broader + variety of servicing equipment are used throughout a simulation. + + Parameters + ---------- + capability : str + A string matching one of ``capability`` options for servicing equipment + dataclasses. + ix : int + The index of the used servicing equipent. + """ + if capability == "CTV": + self.CTV.append(self.CTV.pop(ix)) + elif capability == "SCN": + self.SCN.append(self.SCN.pop(ix)) + elif capability == "LCN": + self.LCN.append(self.LCN.pop(ix)) + elif capability == "CAB": + self.CAB.append(self.CAB.pop(ix)) + elif capability == "RMT": + self.RMT.append(self.RMT.pop(ix)) + elif capability == "DRN": + self.DRN.append(self.DRN.pop(ix)) + elif capability == "DSV": + self.DSV.append(self.DSV.pop(ix)) + elif capability == "TOW": + self.TOW.append(self.TOW.pop(ix)) + elif capability == "AHV": + self.AHV.append(self.AHV.pop(ix)) + else: + # This should not even be able to be reached + raise ValueError( + f"Invalid servicing equipmen capability {capability} has been provided!" + ) + + +
+[docs] +@define(frozen=True, auto_attribs=True) +class PortConfig(FromDictMixin, DateLimitsMixin): + """Port configurations for offshore wind power plant scenarios. + + Parameters + ---------- + name : str + The name of the port, if multiple are used, then be sure this is unique. + tugboats : list[str] + file, or list of files to create the port's tugboats. + + .. note:: Each tugboat is considered to be a tugboat + supporting vessels as + the primary purpose to tow turbines between a repair port and site. + + n_crews : int + The number of service crews available to be working on repairs simultaneously; + each crew is able to service exactly one repair. + crew : ServiceCrew + The crew details, see :py:class:`ServiceCrew` for more information. Dictionary + of labor costs with the following: ``n_day_rate``, ``day_rate``, + ``n_hourly_rate``, and ``hourly_rate``. + max_operations : int + Total number of turbines the port can handle simultaneously. + workday_start : int + The starting hour of a workshift, in 24 hour time. + workday_end : int + The ending hour of a workshift, in 24 hour time. + site_distance : int | float + Distance, in km, a tugboat has to travel to get between site and port. + annual_fee : int | float + The annualized fee for access to the repair port that will be distributed + monthly in the simulation and accounted for on the first of the month from the + start of the simulation to the end of the simulation. + + .. note:: Don't include this cost in both this category and either the + ``FixedCosts.operations_management_administration`` bucket or + ``FixedCosts.marine_management`` category. + + non_operational_start : str | datetime.datetime | None + The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of prohibited operations. When defined at the port level, an undefined or + later starting date will be overridden by the environment, and any associated + tubboats will have this value overridden using the same logic, by default None. + non_operational_end : str | datetime.datetime | None + The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of prohibited operations. When defined at the port level, an undefined + or earlier ending date will be overridden by the environment, and any associated + tubboats will have this value overridden using the same logic, by default None. + reduced_speed_start : str | datetime.datetime | None + The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of reduced speed operations. When defined at the port level, an undefined + or later starting date will be overridden by the environment, and any associated + tubboats will have this value overridden using the same logic, by default None. + reduced_speed_end : str | datetime.datetime | None + The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of reduced speed operations. When defined at the port level, an undefined + or earlier ending date will be overridden by the environment, and any associated + tubboats will have this value overridden using the same logic, by default None. + reduced_speed : float + The maximum operating speed during the annualized reduced speed operations. + When defined at the port level, an undefined or faster value will be overridden + by the environment, and any associated tubboats will have this value overridden + using the same logic, by default 0.0. + """ + + name: str = field(converter=str) + tugboats: list[str | Path] = field(converter=convert_to_list) + crew: ServiceCrew = field(converter=ServiceCrew.from_dict) + n_crews: int = field(default=1, converter=int) + max_operations: int = field(default=1, converter=int) + workday_start: int = field(default=-1, converter=int, validator=valid_hour) + workday_end: int = field(default=-1, converter=int, validator=valid_hour) + site_distance: float = field(default=0.0, converter=float) + annual_fee: float = field( + default=0, converter=float, validator=greater_than_equal_to_zero + ) + non_operational_start: datetime.datetime = field(default=None, converter=parse_date) + non_operational_end: datetime.datetime = field(default=None, converter=parse_date) + reduced_speed_start: datetime.datetime = field(default=None, converter=parse_date) + reduced_speed_end: datetime.datetime = field(default=None, converter=parse_date) + reduced_speed: float = field(default=0, converter=float) + non_operational_dates: pd.DatetimeIndex = field(factory=set, init=False) + reduced_speed_dates: pd.DatetimeIndex = field(factory=set, init=False) + non_operational_dates_set: pd.DatetimeIndex = field(factory=set, init=False) + reduced_speed_dates_set: pd.DatetimeIndex = field(factory=set, init=False) + non_stop_shift: bool = field(default=False, init=False) + + def __attrs_post_init__(self) -> None: + """Post-initialization hook.""" + if self.workday_start == 0 and self.workday_end == 24: + object.__setattr__(self, "non_stop_shift", True)
+ + + +
+[docs] +@define(frozen=True, auto_attribs=True) +class FixedCosts(FromDictMixin): + """Fixed costs for operating a windfarm. All values are assumed to be in $/kW/yr. + + Parameters + ---------- + operations : float + Non-maintenance costs of operating the project. If a value is provided for this + attribute, then it will zero out all other values, otherwise it will be set to + the sum of the remaining values. + operations_management_administration: float + Activities necessary to forecast, dispatch, sell, and manage the production of + power from the plant. Includes both the onsite and offsite personnel, software, + and equipment to coordinate high voltage equipment, switching, port activities, + and marine activities. + + .. note:: This should only be used when not breaking down the cost into the + following categories: ``project_management_administration``, + ``operation_management_administration``, ``marine_management``, and/or + ``weather_forecasting`` + + project_management_administration : float + Financial reporting, public relations, procurement, parts and stock management, + H&SE management, training, subcontracts, and general administration. + marine_management : float + Coordination of port equipment, vessels, and personnel to carry out inspections + and maintenance of generation and transmission equipment. + weather_forecasting : float + Daily forecast of metocean conditions used to plan maintenance visits and + estimate project power production. + condition_monitoring : float + Monitoring of SCADA data from wind turbine components to optimize performance + and identify component faults. + operating_facilities : float + Co-located offices, parts store, quayside facilities, helipad, refueling + facilities, hanger (if necesssary), etc. + environmental_health_safety_monitoring : float + Coordination and monitoring to ensure compliance with HSE requirements during + operations. + insurance : float + Insurance policies during operational period including All Risk Property, + Buisness Interuption, Third Party Liability, and Brokers Fee, and Storm + Coverage. + + .. note:: This should only be used when not breaking down the cost into the + following categories: ``brokers_fee``, ``operations_all_risk``, + ``business_interruption``, ``third_party_liability``, and/or + ``storm_coverage`` + + brokers_fee : float + Fees for arranging the insurance package. + operations_all_risk : float + All Risk Property (physical damage). Sudden and unforseen physical loss or + physical damage to teh plant/assets during the operational phase of a project. + business_interruption : float + Sudden and unforseen loss or physical damage to the plant/assets during the + operational phase of a project causing an interruption. + third_party_liability : float + Liability imposed by law, and/or Express Contractual Liability, for bodily + injury or property damage. + storm_coverage : float + Coverage from huricane and tropical storm events (tyipcally for Atlantic Coast + projects). + annual_leases_fees : float + Ongoing payments, including but not limited to: payments to regulatory body for + permission to operate at project site (terms defined within lease); payments to + Transmission Systems Operators or Transmission Asseet Owners for rights to + transport generated power. + + .. note:: This should only be used when not breaking down the cost into the + following categories: ``submerge_land_lease_costs`` and/or + ``transmission_charges_rights`` + + submerge_land_lease_costs : float + Payments to submerged land owners for rights to build project during operations. + transmission_charges_rights : float + Any payments to Transmissions Systems Operators or Transmission Asset Owners for + rights to transport generated power. + onshore_electrical_maintenance : float + Inspections of cables, transformer, switch gears, power compensation equipment, + etc. and infrequent repairs + + .. warning:: This should only be used if not modeling these as processes within + the model. Currently, onshore modeling is not included. + + labor : float + The costs associated with labor, if not being modeled through the simulated + processes. + """ + + # Total Cost + operations: float = field(default=0) + + # Operations, Management, and General Administration + operations_management_administration: float = field(default=0) + project_management_administration: float = field(default=0) + marine_management: float = field(default=0) + weather_forecasting: float = field(default=0) + condition_monitoring: float = field(default=0) + + operating_facilities: float = field(default=0) + environmental_health_safety_monitoring: float = field(default=0) + + # Insurance + insurance: float = field(default=0) + brokers_fee: float = field(default=0) + operations_all_risk: float = field(default=0) + business_interruption: float = field(default=0) + third_party_liability: float = field(default=0) + storm_coverage: float = field(default=0) + + # Annual Leases and Fees + annual_leases_fees: float = field(default=0) + submerge_land_lease_costs: float = field(default=0) + transmission_charges_rights: float = field(default=0) + + onshore_electrical_maintenance: float = field(default=0) + + labor: float = field(default=0) + + resolution: dict = field(init=False) + hierarchy: dict = field(init=False) + + def cost_category_validator(self, name: str, sub_names: list[str]): + """Either updates the higher-level cost to be the sum of the category's + lower-level costs or uses a supplied higher-level cost and zeros out the + lower-level costs. + + Parameters + ---------- + name : str + The higher-level cost category's name. + sub_names : List[str] + The lower-level cost names associated with the higher-level category. + """ + if getattr(self, name) <= 0: + object.__setattr__(self, name, fsum(getattr(self, el) for el in sub_names)) + else: + for cost in sub_names: + object.__setattr__(self, cost, 0) + + def __attrs_post_init__(self): + """Post-initialization.""" + grouped_names = { + "operations_management_administration": [ + "project_management_administration", + "marine_management", + "weather_forecasting", + "condition_monitoring", + ], + "insurance": [ + "brokers_fee", + "operations_all_risk", + "business_interruption", + "third_party_liability", + "storm_coverage", + ], + "annual_leases_fees": [ + "submerge_land_lease_costs", + "transmission_charges_rights", + ], + } + individual_names = [ + "operating_facilities", + "environmental_health_safety_monitoring", + "onshore_electrical_maintenance", + "labor", + ] + + # Check each category for aggregation + for _name, _sub_names in grouped_names.items(): + self.cost_category_validator(_name, _sub_names) + + # Check for single value aggregation + self.cost_category_validator("operations", [*grouped_names] + individual_names) + + # Create the value resolution mapping + object.__setattr__( + self, + "resolution", + { + "low": ["operations"], + "medium": [ + "operations_management_administration", + "insurance", + "annual_leases_fees", + "operating_facilities", + "environmental_health_safety_monitoring", + "onshore_electrical_maintenance", + "labor", + ], + "high": [ + "project_management_administration", + "marine_management", + "weather_forecasting", + "condition_monitoring", + "brokers_fee", + "operations_all_risk", + "business_interruption", + "third_party_liability", + "storm_coverage", + "submerge_land_lease_costs", + "transmission_charges_rights", + "operating_facilities", + "environmental_health_safety_monitoring", + "onshore_electrical_maintenance", + "labor", + ], + }, + ) + + object.__setattr__( + self, + "hierarchy", + { + "operations": { + "operations_management_administration": [ + "project_management_administration", + "marine_management", + "weather_forecasting", + "condition_monitoring", + ], + "insurance": [ + "brokers_fee", + "operations_all_risk", + "business_interruption", + "third_party_liability", + "storm_coverage", + ], + "annual_leases_fees": [ + "submerge_land_lease_costs", + "transmission_charges_rights", + ], + "operating_facilities": [], + "environmental_health_safety_monitoring": [], + "onshore_electrical_maintenance": [], + "labor": [], + } + }, + )
+ + + +
+[docs] +@define(auto_attribs=True) +class SubString: + """A list of the upstream connections for a turbine and its downstream connector. + + Parameters + ---------- + downstream : str + The downstream turbine/substation connection id. + upstream : list[str] + A list of the upstream turbine connections. + """ + + downstream: str + upstream: list[str]
+ + + +
+[docs] +@define(auto_attribs=True) +class String: + """All of the connection information for a complete string in a wind farm. + + Parameters + ---------- + start : str + The substation's ID (``System.id``) + upstream_map : dict[str, SubString] + The dictionary of each turbine ID in the string and it's upstream ``SubString``. + """ + + start: str + upstream_map: dict[str, SubString]
+ + + +
+[docs] +@define(auto_attribs=True) +class SubstationMap: + """A mapping of every ``String`` connected to a substation, excluding export + connections to other substations. + + Parameters + ---------- + string_starts : list[str] + A list of every first turbine's ``System.id`` in a string connected to the + substation. + string_map : dict[str, String] + A dictionary mapping each string starting turbine to its ``String`` data. + downstream : str + The ``System.id`` of where the export cable leads. This should be the same + ``System.id`` as the substation for an interconnection point, or another + connecting substation. + """ + + string_starts: list[str] + string_map: dict[str, String] + downstream: str
+ + + +
+[docs] +@define(auto_attribs=True) +class WindFarmMap: + """A list of the upstream connections for a turbine and its downstream connector. + + Parameters + ---------- + substation_map : list[str] + A dictionary mapping of each substation and its ``SubstationMap``. + export_cables : list[tuple[str, str]] + A list of the export cable connections. + """ + + substation_map: dict[str, SubstationMap] + export_cables: list[tuple[str, str]] + +
+[docs] + def get_upstream_connections( + self, substation: str, string_start: str, node: str, return_cables: bool = True + ) -> list[str] | tuple[list[str], list[str]]: + """Retrieve the upstream turbines (and optionally cables) within the wind farm + graph. + + Parameters + ---------- + substation : str + The substation's ``System.id``. + string_start : str + The ``System.id`` of the first turbine in the string. + node : str + The ``System.id`` of the ending node for a cable connection. + return_cables : bool + Indicates if the ``Cable.id`` should be generated for each of the turbines, + by default True. + + Returns + ------- + list[str] | tuple[list[str], list[str]] + A list of ``System.id`` for all of the upstream turbines of ``node`` if + ``cables=False``, otherwise the upstream turbine and the ``Cable.id`` lists + are returned. + """ + strings = self.substation_map[substation].string_map + upstream = strings[string_start].upstream_map + turbines = upstream[node].upstream + if return_cables: + cables = [ + f"cable::{node if i == 0 else turbines[i - 1]}::{t}" + for i, t in enumerate(turbines) + ] + return turbines, cables + return turbines
+ + +
+[docs] + def get_upstream_connections_from_substation( + self, substation: str, return_cables: bool = True, by_string: bool = True + ) -> ( + list[str] + | tuple[list[str], list[str]] + | list[list[str]] + | tuple[list[list[str]], list[list[str]]] + ): + """Retrieve the upstream turbines (and optionally, cables) connected to a + py:attr:`substation` in the wind farm graph. + + Parameters + ---------- + substation : str + The py:attr:`System.id` for the substation. + return_cables : bool, optional + Indicates if the ``Cable.id`` should be generated for each of the turbines, + by default True + by_string : bool, optional + Indicates if the list of turbines (and cables) should be a nested list for + each string (py:obj:`True`), or as 1-D list (py:obj:`False`), by default + True. + + Returns + ------- + list[str] | tuple[list[str], list[str]] + A list of ``System.id`` for all of the upstream turbines of ``node`` if + ``return_cables=False``, otherwise the upstream turbine and the ``Cable.id`` + lists are returned. These are bifurcated in lists of lists for each string + if ``by_string=True`` + """ + turbines = [] + cables = [] + substation_map = self.substation_map[substation] + start_nodes = substation_map.string_starts + for start_node in start_nodes: + # Add the starting node of the string and substation-turbine array cable + _turbines = [start_node] + _cables = [f"cable::{substation}::{start_node}"] + + # Add the main components of the string + _t, _c = self.get_upstream_connections(substation, start_node, start_node) + _turbines.extend(_t) + _cables.extend(_c) + turbines.append(_turbines) + cables.append(_cables) + + if not by_string: + turbines = [el for string in turbines for el in string] # type: ignore + cables = [el for string in cables for el in string] # type: ignore + + if return_cables: + return turbines, cables + return turbines
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/wombat/core/environment.html b/_modules/wombat/core/environment.html new file mode 100644 index 00000000..45a440b0 --- /dev/null +++ b/_modules/wombat/core/environment.html @@ -0,0 +1,1435 @@ + + + + + + + + + + wombat.core.environment — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for wombat.core.environment

+"""Provides the O&M Enviroment class; a subclass of simpy.Environment."""
+from __future__ import annotations
+
+import io
+import csv
+import math
+import logging
+import datetime as dt
+from typing import TYPE_CHECKING
+from pathlib import Path
+from datetime import datetime, timedelta
+
+import numpy as np
+import simpy
+import pandas as pd
+import polars as pl
+import pyarrow as pa
+import pyarrow.csv  # pylint: disable=W0611
+from simpy.events import Event
+
+import wombat  # pylint: disable=W0611
+from wombat.utilities import hours_until_future_hour
+from wombat.core.data_classes import parse_date
+
+
+if TYPE_CHECKING:
+    from wombat.windfarm import Windfarm
+
+
+EVENTS_COLUMNS = [
+    "datetime",
+    "env_datetime",
+    "env_time",
+    "agent",
+    "action",
+    "reason",
+    "additional",
+    "system_id",
+    "system_name",
+    "part_id",
+    "part_name",
+    "system_operating_level",
+    "part_operating_level",
+    "duration",
+    "distance_km",
+    "request_id",
+    "location",
+    "materials_cost",
+    "hourly_labor_cost",
+    "salary_labor_cost",
+    "total_labor_cost",
+    "equipment_cost",
+    "total_cost",
+]
+
+
+
+[docs] +class WombatEnvironment(simpy.Environment): + """The primary mechanism for powering an O&M simulation. This object has insight + into all other simulation objects, and controls the timing, date/time stamps, and + weather conditions. + + Parameters + ---------- + data_dir : pathlib.Path | str + Directory where the inputs are stored and where to save outputs. + weather_file : str + Name of the weather file. Should be contained within ``data_dir``/weather/, with + columns "datetime", "windspeed", and, optionally, "waveheight". The datetime + column should adhere to the following format: "MM/DD/YY HH:MM", in 24-hour time. + workday_start : int + Starting time for the repair crew, in 24 hour local time. This can be overridden + by an ``ServiceEquipmentData`` object that operates outside of the "typical" + working hours. + workday_end : int + Ending time for the repair crew, in 24 hour local time. This can be overridden + by an ``ServiceEquipmentData`` object that operates outside of the "typical" + working hours. + simulation_name : str | None, optional + Name of the simulation; will be used for naming the log file, by default None. + If ``None``, then the current time will be used. Will always save to + ``data_dir``/outputs/logs/``simulation_name``.log. + + .. note: spaces (" ") will be replaced with underscores ("_"), for example: + "my example analysis" becomes "my_example_analysis". + + start_year : int | None, optional + Custom starting year for the weather profile, by default None. If ``None`` or + less than the first year of the weather profile, this will be ignored. + end_year : int | None, optional + Custom ending year for the weather profile, by default None. If ``None`` or + greater than the last year of the weather profile, this will be ignored. + port_distance : int | float + The simulation-wide daily travel distance for servicing equipment. This + should be used as a base setting when multiple or all servicing equipment + will be operating out of the same base location, but can be individually + modified. + non_operational_start : str | datetime.datetime | None + The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of prohibited operations. When defined at the environment level, + an undefined or later starting date will be overridden for all servicing + equipment and any modeled port, by default None. + non_operational_end : str | datetime.datetime | None + The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of prohibited operations. When defined at the environment level, + an undefined or earlier ending date will be overridden for all servicing + equipment and any modeled port, by default None. + reduced_speed_start : str | datetime.datetime | None + The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of reduced speed operations. When defined at the environment level, + an undefined or later starting date will be overridden for all servicing + equipment and any modeled port, by default None. + reduced_speed_end : str | datetime.datetime | None + The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of reduced speed operations. When defined at the environment level, + an undefined or earlier ending date will be overridden for all servicing + equipment and any modeled port, by default None. + reduced_speed : float + The maximum operating speed during the annualized reduced speed operations. + When defined at the environment level, an undefined or faster value will be + overridden for all servicing equipment and any modeled port, by default 0.0. + random_seed : int | None + The random seed to be passed to a universal NumPy ``default_rng`` object to + generate Weibull random generators, by default None. + random_generator: np.random._generator.Generator | None + An optional numpy random generator that can be provided to seed a simulation + with the same generator each time, in place of the random seed. If a + :py:attr:`random_seed` is also provided, this will override the random seed, + by default None. + + Raises + ------ + FileNotFoundError + Raised if ``data_dir`` cannot be found. + """ + + def __init__( + self, + data_dir: Path | str, + weather_file: str, + workday_start: int, + workday_end: int, + simulation_name: str | None = None, + start_year: int | None = None, + end_year: int | None = None, + port_distance: int | float | None = None, + non_operational_start: str | dt.datetime | None = None, + non_operational_end: str | dt.datetime | None = None, + reduced_speed_start: str | dt.datetime | None = None, + reduced_speed_end: str | dt.datetime | None = None, + reduced_speed: float = 0.0, + random_seed: int | None = None, + random_generator: np.random._generator.Generator | None = None, + ) -> None: + """Initialization.""" + super().__init__() + self.data_dir = Path(data_dir).resolve() + if not self.data_dir.is_dir(): + raise FileNotFoundError(f"{self.data_dir} does not exist") + + self.workday_start = int(workday_start) + self.workday_end = int(workday_end) + if not 0 <= self.workday_start <= 24: + raise ValueError("workday_start must be a valid 24hr time before midnight.") + if not 0 <= self.workday_end <= 24: + raise ValueError("workday_end must be a valid 24hr time.") + if self.workday_end <= self.workday_start: + raise ValueError( + "Work shifts must end after they start ({self.workday_start}hrs)." + ) + + self.port_distance = port_distance + self.weather = self._weather_setup(weather_file, start_year, end_year) + self.weather_dates = pd.DatetimeIndex( + self.weather.get_column("datetime").to_pandas() + ).to_pydatetime() + self.max_run_time = self.weather.shape[0] + self.shift_length = self.workday_end - self.workday_start + + # Set the environmental consideration parameters + self.non_operational_start = parse_date(non_operational_start) + self.non_operational_end = parse_date(non_operational_end) + self.reduced_speed_start = parse_date(reduced_speed_start) + self.reduced_speed_end = parse_date(reduced_speed_end) + self.reduced_speed = reduced_speed + + if random_generator is not None: + self.random_generator = random_generator + self.random_seed = None + elif random_seed is not None: + self.random_seed = random_seed + self.random_generator = np.random.default_rng(seed=random_seed) + else: + self.random_seed = None + self.random_generator = np.random.default_rng() + + self.simulation_name = simulation_name + self._logging_setup() + self.process(self._log_actions()) + +
+[docs] + def _register_windfarm(self, windfarm: Windfarm) -> None: + """Adds the simulation windfarm to the class attributes.""" + self.windfarm = windfarm
+ + +
+[docs] + def run(self, until: int | float | Event | None = None): + """Extends the ``simpy.Environment.run`` method to change the default behavior + if no argument is passed to ``until``, which will now run a simulation until the + end of the weather profile is reached. + + Parameters + ---------- + until : Optional[Union[int, float, Event]], optional + When to stop the simulation, by default None. See documentation on + ``simpy.Environment.run`` for more details. + """ + # If running a paused simulation, then reopen the file and append, but only if + # the simulation time is lower than the upper bound + time_check = self.now < self.max_run_time + if self._events_csv.closed and time_check: # type: ignore + self._events_csv = open(self.events_log_fname, "a") + self._events_writer = csv.DictWriter( + self._events_csv, delimiter="|", fieldnames=EVENTS_COLUMNS + ) + if hasattr(self, "windfarm") and self._operations_csv.closed and time_check: + self._operations_csv: io.TextIOWrapper = open( + self.operations_log_fname, "a" + ) + self.windfarm._setup_logger(initial=False) + + if until is None: + until = self.max_run_time + elif until > self.max_run_time: + until = self.max_run_time + try: + super().run(until=until) + except BaseException as e: + # Flush the logs to so the simulation up to the point of failure is logged + self._events_writer.writerows(self._events_buffer) + self._events_buffer.clear() + self._events_csv.close() + self._operations_writer.writerows(self._operations_buffer) + self._operations_buffer.clear() + self._operations_csv.close() + print( + f"Simulation failed at hour {self.now:,.6f}," + f" simulation time: {self.simulation_time}" + ) + raise e + + # Ensure all logged events make it to their target file + self._events_writer.writerows(self._events_buffer) + self._events_buffer.clear() + self._events_csv.close() + self._operations_writer.writerows(self._operations_buffer) + self._operations_buffer.clear() + self._operations_csv.close()
+ + +
+[docs] + def _logging_setup(self) -> None: + """Completes the setup for logging data.""" + if self.simulation_name is None: + self.simulation_name = simulation = "wombat" + else: + simulation = self.simulation_name.replace(" ", "_") + dt_stamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + + events_log_fname = f"{dt_stamp}_{simulation}_events.csv" + operations_log_fname = f"{dt_stamp}_{simulation}_operations.csv" + power_potential_fname = f"{dt_stamp}_{simulation}_power_potential.csv" + power_production_fname = f"{dt_stamp}_{simulation}_power_production.csv" + metrics_input_fname = f"{dt_stamp}_{simulation}_metrics_inputs.yaml" + + log_path = self.data_dir / "results" + if not log_path.exists(): + log_path.mkdir() + self.events_log_fname = log_path / events_log_fname + self.operations_log_fname = log_path / operations_log_fname + self.power_potential_fname = log_path / power_potential_fname + self.power_production_fname = log_path / power_production_fname + self.metrics_input_fname = log_path / metrics_input_fname + + _dir = self.data_dir / "results" + if not _dir.is_dir(): + _dir.mkdir() + + self._events_csv = open(self.events_log_fname, "w") + self._operations_csv = open(self.operations_log_fname, "w") + self._events_writer = csv.DictWriter( + self._events_csv, delimiter="|", fieldnames=EVENTS_COLUMNS + ) + self._events_writer.writeheader() + + self._events_buffer: list[dict] = [] + self._operations_buffer: list[dict] = []
+ + +
+[docs] + def get_random_seconds(self, low: int = 0, high: int = 10) -> float: + """Generate a random number of seconds to wait, between :py:attr:`low` and + :py:attr:`high`. + + Parameters + ---------- + low : int, optional + Minimum number of seconds to wait, by default 0. + high : int, optional + Maximum number of seconds to wait, by default 10. + + Returns + ------- + float + Number of seconds to wait. + """ + seconds_to_wait, *_ = ( + self.random_generator.integers(low=low, high=high, size=1) / 3600.0 + ) + return seconds_to_wait
+ + + @property + def simulation_time(self) -> datetime: + """Current time within the simulation ("datetime" column within weather).""" + now = self.now + minutes = now % 1 * 60 + if now == self.max_run_time: + _dt = self.weather_dates[math.floor(now - 1)] + _dt + timedelta(hours=1) + else: + _dt = self.weather_dates[math.floor(now)] + + minutes, seconds = math.floor(minutes), math.ceil(minutes % 1 * 60) + return _dt + timedelta(minutes=minutes, seconds=seconds) + +
+[docs] + def is_workshift(self, workday_start: int = -1, workday_end: int = -1) -> bool: + """Check if the current simulation time is within the windfarm's working hours. + + Parameters + ---------- + workday_start : int + A valid hour in 24 hour time, by default -1. This should only be provided + from an ``ServiceEquipmentData`` object. ``workday_end`` must also be + provided in order to be used. + workday_end : int + A valid hour in 24 hour time, by default -1. This should only be provided + from an ``ServiceEquipmentData`` object. ``workday_start`` must also be + provided in order to be used. + + Returns + ------- + bool + True if it's valid working hours, False otherwise. + """ + if -1 in (workday_start, workday_end): + # Return True if the shift is around the clock + if self.workday_start == 0 and self.workday_end == 24: + return True + return self.workday_start <= self.simulation_time.hour < self.workday_end + + # Return true if the shift is around the clock + if workday_start == 0 and workday_end == 24: + return True + + return workday_start <= self.simulation_time.hour < workday_end
+ + +
+[docs] + def hour_in_shift( + self, hour: int, workday_start: int = -1, workday_end: int = -1 + ) -> bool: + """Checks whether an ``hour`` is within the working hours. + + Parameters + ---------- + hour : int + Hour of the day. + workday_start : int + A valid hour in 24 hour time, by default -1. This should only be provided + from an ``ServiceEquipmentData`` object. ``workday_end`` must also be + provided in order to be used. + workday_end : int + A valid hour in 24 hour time, by default -1. This should only be provided + from an ``ServiceEquipmentData`` object. ``workday_start`` must also be + provided in order to be used. + + Returns + ------- + bool + True if ``hour`` is during working hours, False otherwise. + """ + if -1 in (workday_start, workday_end): + return self.workday_start <= hour < self.workday_end + return workday_start <= hour < workday_end
+ + +
+[docs] + def hours_to_next_shift(self, workday_start: int = -1) -> float: + """Time until the next work shift starts, in hours. + + Parameters + ---------- + workday_start : int + A valid hour in 24 hour time, by default -1. This should only be provided + from an ``ServiceEquipmentData`` object. + + Returns + ------- + float + Hours until the next shift starts. + """ + current = self.simulation_time + start = self.workday_start if workday_start == -1 else workday_start + if current.hour < start: + # difference between now and workday start + return hours_until_future_hour(current, start) + elif current.hour == start == 0: + # Need to manually move forward one whole day to avoid an infinite loop + return hours_until_future_hour(current, 24) + else: + # time to midnight + hour of workday start + return start + hours_until_future_hour(current, 0)
+ + + @property + def current_time(self) -> str: + """Timestamp for the current time as a datetime.datetime.strftime.""" + return datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f") + +
+[docs] + def date_ix(self, date: dt.datetime | dt.date) -> int: + """The first index of a future date. This corresponds to the number of hours + until this dates from the very beginning of the simulation. + + Parameters + ---------- + date : datetime.datetime | datetime.date + A date within the environment's simulation range. + + Returns + ------- + int + Index of the weather profile corresponds to the first hour of ``date``. + """ + if isinstance(date, dt.datetime): + date = date.date() + ix, *_ = self.weather.filter(pl.col("datetime") == date) + return ix.item()
+ + +
+[docs] + def _weather_setup( + self, + weather_file: str, + start_year: int | None = None, + end_year: int | None = None, + ) -> pl.DataFrame: + """Reads the weather data from the "<inputs>/weather" directory, and creates the + ``start_date`` and ``end_date`` time stamps for the simulation. + + This also fills any missing data with zeros and interpolates the values of any + missing datetime entries. + + Parameters + ---------- + weather_file : str + Name of the weather file to be used by the environment. Should be contained + within ``data_dir/weather``. + start_year : Optional[int], optional + Custom starting year for the weather profile, by default None. If ``None`` + or less than the first year of the weather profile, this will be ignored. + end_year : Optional[int], optional + Custom ending year for the weather profile, by default None. If ``None`` or + greater than the last year of the weather profile, this will be ignored. + + Returns + ------- + pd.DataFrame + The wind (and wave) timeseries. + """ + REQUIRED = ["windspeed", "waveheight"] + + # PyArrow datetime conversion setup + convert_options = pa.csv.ConvertOptions( + timestamp_parsers=[ + "%m/%d/%y %H:%M", + "%m/%d/%y %I:%M", + "%m/%d/%y %H:%M:%S", + "%m/%d/%y %I:%M:%S", + "%m/%d/%Y %H:%M", + "%m/%d/%Y %I:%M", + "%m/%d/%Y %H:%M:%S", + "%m/%d/%Y %I:%M:%S", + "%m-%d-%y %H:%M", + "%m-%d-%y %I:%M", + "%m-%d-%y %H:%M:%S", + "%m-%d-%y %I:%M:%S", + "%m-%d-%Y %H:%M", + "%m-%d-%Y %I:%M", + "%m-%d-%Y %H:%M:%S", + "%m-%d-%Y %I:%M:%S", + "%Y-%m-%d %H:%M", + "%Y-%m-%d %I:%M", + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d %I:%M:%S", + ] + ) + weather = ( + pl.from_pandas( + pa.csv.read_csv( + self.data_dir / "weather" / weather_file, + convert_options=convert_options, + ) + .to_pandas() + .fillna(0.0) + .set_index("datetime") + .sort_index() + .resample("H") + .interpolate(limit_direction="both") # , limit=5) + .reset_index(drop=False) + ) + .with_row_count() + .with_columns( + [ + pl.col("datetime").cast(pl.Datetime).dt.cast_time_unit("ns"), + (pl.col("datetime").dt.hour()).alias("hour"), + ] + ) + ) + + missing = set(REQUIRED).difference(weather.columns) + if missing: + raise KeyError( + "The weather data are missing the following required columns:" + f" {missing}" + ) + + # Create the start and end points + self.start_datetime = weather.get_column("datetime").dt.min() + self.end_datetime = weather.get_column("datetime").dt.max() + self.start_year = self.start_datetime.year + self.end_year = self.end_datetime.year + + if start_year is None and end_year is None: + return weather + + if start_year is None: + pass + elif start_year > self.end_year: + raise ValueError( + f"'start_year' ({start_year}) occurs after the last available year" + f" in the weather data (range: {self.end_year})" + ) + else: + # Filter for the provided, validated starting year and update the attribute + weather = weather.filter(pl.col("datetime").dt.year() >= start_year) + self.start_datetime = weather.get_column("datetime").dt.min() + start_year = self.start_year = self.start_datetime.year + + if end_year is None: + pass + elif start_year is None and end_year < self.start_year: + raise ValueError( + f"The provided 'end_year' ({end_year}) is before the start_year" + f" ({self.start_year})" + ) + elif start_year is not None: + if end_year < start_year: + raise ValueError( + f"The provided 'end_year' ({end_year}) is before the start_year" + f" ({start_year})" + ) + else: + # Filter for the provided, validated ending year and update + weather = weather.filter(pl.col("datetime").dt.year() <= end_year) + self.end_datetime = weather.get_column("datetime").dt.max() + self.end_year = self.end_datetime.year + else: + # Filter for the provided, validated ending year and update the attribute + weather = weather.filter(pl.col("datetime").dt.year() <= end_year) + self.end_datetime = weather.get_column("datetime").dt.max() + self.end_year = self.end_datetime.year + + column_order = weather.columns + column_order.insert(0, column_order.pop(column_order.index("hour"))) + column_order.insert(0, column_order.pop(column_order.index("waveheight"))) + column_order.insert(0, column_order.pop(column_order.index("windspeed"))) + column_order.insert(0, column_order.pop(column_order.index("datetime"))) + column_order.insert(0, column_order.pop(column_order.index("row_nr"))) + + # Ensure the columns are ordered correctly and re-compute pandas-compatible ix + return weather.select(column_order).drop(columns="row_nr").with_row_count()
+ + + @property + def weather_now(self) -> pl.DataFrame: + """The current weather. + + Returns + ------- + pl.DataFrame + A length 1 slice from the weather profile at the current ``int()`` rounded + hour, in simulation time. + """ + # Rounds down because we won't arrive at the next weather event until that hour + now = int(self.now) + return self.weather.slice(now, 1) + +
+[docs] + def weather_forecast( + self, hours: int | float + ) -> tuple[pl.Series, pl.Series, pl.Series, pl.Series]: + """Returns the datetime, wind, wave, and hour data for the next ``hours`` hours, + starting from the current hour's weather. + + Parameters + ---------- + hours : Union[int, float] + Number of hours to look ahead, rounds up to the nearest hour. + + Returns + ------- + tuple[pl.Series, pl.Series, pl.Series, pl.Series] + Each of the relevant columns (datetime, wind, wave, hour) from the weather + profile. + """ + # If it's not on the hour, ensure we're looking ``hours`` hours into the future + start = math.floor(self.now) + _, ix, wind, wave, hour, *_ = self.weather.slice( + start, math.ceil(hours) + math.ceil(self.now % 1) + ) + return ix, hour, wind, wave
+ + +
+[docs] + def log_action( + self, + *, + agent: str, + action: str, + reason: str, + additional: str = "", + system_id: str = "", + system_name: str = "", + part_id: str = "", + part_name: str = "", + system_ol: float | int = 0, + part_ol: float | int = 0, + duration: float = 0, + distance_km: float = 0, + request_id: str = "na", + location: str = "na", + materials_cost: int | float = 0, + hourly_labor_cost: int | float = 0, + salary_labor_cost: int | float = 0, + equipment_cost: int | float = 0, + ) -> None: + """Formats the logging messages into the expected format for logging. + + Parameters + ---------- + agent : str + Agent performing the action. + action : str + Action that was taken. + reason : str + Reason an action was taken. + additional : str + Any additional information that needs to be logged. + system_id : str + Turbine ID, ``System.id``, by default "". + system_name : str + Turbine name, ``System.name``, by default "". + part_id : str + Subassembly, component, or cable ID, ``_.id``, by default "". + part_name : str + Subassembly, component, or cable name, ``_.name``, by default "". + system_ol : float | int + Turbine operating level, ``System.operating_level``. Use an empty string + for n/a, by default 0. + part_ol : float | int + Subassembly, component, or cable operating level, ``_.operating_level``. Use + an empty string for n/a, by default 0. + request_id : str + The ``RepairManager`` assigned request_id found in + ``RepairRequest.request_id``, by default "na". + location : str + The location of where the event ocurred: should be one of site, port, + enroute, or system, by default "na". + duration : float + Length of time the action lasted, by default 0. + distance : float + Distance traveled, in km, if applicable, by default 0. + materials_cost : Union[int, float], optional + Total cost of materials for action, in USD, by default 0. + hourly_labor_cost : Union[int, float], optional + Total cost of hourly labor for action, in USD, by default 0. + salary_labor_cost : Union[int, float], optional + Total cost of salaried labor for action, in USD, by default 0. + equipment_cost : Union[int, float], optional + Total cost of equipment for action, in USD, by default 0. + """ + valid_locations = ("site", "system", "port", "enroute", "na") + if location not in valid_locations: + raise ValueError( + f"Event logging `location` must be one of: {valid_locations}" + ) + total_labor_cost = hourly_labor_cost + salary_labor_cost + total_cost = total_labor_cost + equipment_cost + materials_cost + now = self.simulation_time + row = { + "datetime": dt.datetime.now(), + "env_datetime": now, + "env_time": self.now, + "system_id": system_id, + "system_name": system_name, + "part_id": part_id, + "part_name": part_name, + "system_operating_level": system_ol, + "part_operating_level": part_ol, + "agent": agent, + "action": action, + "reason": reason, + "additional": additional, + "duration": duration, + "distance_km": distance_km, + "request_id": request_id, + "location": location, + "materials_cost": materials_cost, + "hourly_labor_cost": hourly_labor_cost, + "salary_labor_cost": salary_labor_cost, + "equipment_cost": equipment_cost, + "total_labor_cost": total_labor_cost, + "total_cost": total_cost, + } + # Don't log the initiation of a crew transfer that can forced at the end of an + # operation but happens to be after the end of the simulation + if now <= self.end_datetime: + self._events_buffer.append(row)
+ + +
+[docs] + def _log_actions(self): + """Writes the action log items every 8000 hours.""" + HOURS = 8000 + while True: + yield self.timeout(HOURS) + self._events_writer.writerows(self._events_buffer) + self._events_buffer.clear()
+ + +
+[docs] + def load_events_log_dataframe(self) -> pd.DataFrame: + """Imports the logging file created in ``run`` and returns it as a formatted + ``pandas.DataFrame``. + + Returns + ------- + pd.DataFrame + The formatted logging data from a simulation. + """ + log_df = ( + pd.read_csv( + self.events_log_fname, + delimiter="|", + engine="pyarrow", + dtype={ + "agent": "string", + "action": "string", + "reason": "string", + "additional": "string", + "system_id": "string", + "system_name": "string", + "part_id": "string", + "part_name": "string", + "request_id": "string", + "location": "string", + }, + ) + .set_index("datetime") + .sort_index() + ) + return log_df
+ + +
+[docs] + def _calculate_windfarm_total( + self, op: pd.DataFrame, prod: pd.DataFrame | None = None + ) -> pd.DataFrame: + """Calculates the overall wind farm operational level, accounting for substation + downtime by multiplying the sum of all downstream turbine operational levels by + the substation's operational level. + + Parameters + ---------- + op : pd.DataFrame + The turbine and substation operational level DataFrame. + + Notes + ----- + This is a crude cap on the operations, and so a smarter way of capping + the availability should be added in the future. + + Returns + ------- + pd.DataFrame + The aggregate wind farm operational level. + """ + t_id = self.windfarm.turbine_id + turbines = self.windfarm.turbine_weights[t_id].values * op[t_id] + total = np.sum( + [ + op[[sub]] + * np.array( + [ + [math.fsum(row)] + for _, row in turbines[val["turbines"]].iterrows() + ] + ).reshape(-1, 1) + for sub, val in self.windfarm.substation_turbine_map.items() + ], + axis=0, + ) + return total
+ + +
+[docs] + def _calculate_adjusted_production( + self, op: pd.DataFrame, prod: pd.DataFrame + ) -> pd.DataFrame: + """Calculates the overall wind farm power production and adjusts individual + turbine production by accounting for substation downtime. This is done by + multiplying the all downstream turbine operational levels by the substation's + operational level. + + Parameters + ---------- + op : pd.DataFrame + The operational level DataFrame with turbine, substation, and windfarm + columns. + prod : pd.DataFrame + The turbine energy production DataFrame. + + Notes + ----- + This is a crude cap on the operations, and so a smarter way of capping + the availability should be added in the future. + + Returns + ------- + pd.DataFrame + Either the aggregate wind farm operational level or the total wind farm + energy production if the :py:attr:`prod` is provided. + """ + # Adjust individual turbine production for substation downtime + prod = prod.copy() + for sub, val in self.windfarm.substation_turbine_map.items(): + prod[val["turbines"]] *= op[[sub]].values + prod.windfarm = prod[self.windfarm.turbine_id].sum(axis=1) + return prod[["windfarm"]]
+ + +
+[docs] + def load_operations_log_dataframe(self) -> pd.DataFrame: + """Imports the logging file created in ``run`` and returns it as a formatted + ``pandas.DataFrame``. + + Returns + ------- + pd.DataFrame + The formatted logging data from a simulation. + """ + log_df = ( + pd.read_csv( + self.operations_log_fname, + delimiter="|", + engine="pyarrow", + ) + .set_index("datetime") + .sort_values("datetime") + ) + log_df["windfarm"] = self._calculate_windfarm_total(log_df) + return log_df
+ + +
+[docs] + def power_production_potential_to_csv( # type: ignore + self, + windfarm: wombat.windfarm.Windfarm, + operations: pd.DataFrame | None = None, + return_df: bool = True, + ) -> tuple[pd.DataFrame, pd.DataFrame]: + """Creates the power production ``DataFrame`` and optionally returns it. + + Parameters + ---------- + windfarm : wombat.windfarm.Windfarm + The simulation's windfarm object. + operations : Optional[pd.DataFrame], optional + The operations log ``DataFrame`` if readily available, by default None. If + ``None``, then it will be created through + ``load_operations_log_dataframe()``. + return_df : bool, optional + Indicator to return the power production for further usage, by default True. + + Returns + ------- + Tuple[pd.DataFrame, pd.DataFrame] + The power potential and production timeseries data. + """ + write_options = pa.csv.WriteOptions(delimiter="|") + + if operations is None: + operations = self.load_operations_log_dataframe().sort_values("env_time") + + turbines = windfarm.turbine_id + windspeed = self.weather.to_pandas().set_index("datetime").windspeed + windspeed = windspeed.loc[operations.env_datetime].values + potential_df = pd.DataFrame( + [], + index=operations.env_datetime, + columns=["env_time", "env_datetime", "windspeed", "windfarm"] + + turbines.tolist(), + ) + potential_df[turbines] = np.vstack( + [windfarm.system(t_id).power(windspeed) for t_id in turbines] + ).T + potential_df = potential_df.assign( + windspeed=windspeed, + windfarm=potential_df[turbines].sum(axis=1), + env_time=operations.env_time.values, + env_datetime=operations.env_datetime.values, + ) + pa.csv.write_csv( + pa.Table.from_pandas(potential_df), + self.power_potential_fname, + write_options=write_options, + ) + + # TODO: The actual windfarm production needs to be clipped at each subgraph to + # the max of the substation's operating capacity and then summed. + production_df = potential_df.copy() + production_df[turbines] *= operations[turbines].values + production_df.windfarm = self._calculate_adjusted_production( + operations, production_df + ) + pa.csv.write_csv( + pa.Table.from_pandas(production_df), + self.power_production_fname, + write_options=write_options, + ) + if return_df: + return potential_df, production_df
+ + +
+[docs] + def cleanup_log_files(self) -> None: + """Convenience method to clear the output log files in case a large + batch of simulations is being run and there are space limitations. + + ... warning:: This shuts down the loggers, so no more logging will be able + to be performed. + """ + # NOTE: Everything is wrapped in a try/except clause to protect against failure + # when inevitably a file has already been deleted on accident, or if in the + # dataframe generation step, the original logs were deleted + + logging.shutdown() + if not self._events_csv.closed: + self._events_csv.close() + if not self._operations_csv.closed: + self._operations_csv.close() + + try: + self.events_log_fname.unlink() + except FileNotFoundError: + pass + + try: + self.operations_log_fname.unlink() + except FileNotFoundError: + pass + + try: + self.power_potential_fname.unlink() + except FileNotFoundError: + pass + + try: + self.power_production_fname.unlink() + except FileNotFoundError: + pass + + try: + self.metrics_input_fname.unlink() + except FileNotFoundError: + pass
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/wombat/core/port.html b/_modules/wombat/core/port.html new file mode 100644 index 00000000..36f1ef23 --- /dev/null +++ b/_modules/wombat/core/port.html @@ -0,0 +1,991 @@ + + + + + + + + + + wombat.core.port — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for wombat.core.port

+"""Creates the `Port` class that provies the tow-to-port repair capabilities for
+offshore floating wind farms. The `Port` will control a series of tugboats enabled
+through the "TOW" capability that get automatically dispatched once a tow-to-port repair
+is submitted and a tugboat is available (`ServiceEquipment.at_port`). The `Port` also
+controls any mooring repairs through the "AHV" capability, which operates similarly to
+the tow-to-port except that it will not be released until the repair is completed, and
+operates on a strict shift scheduling basis.
+"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+from pathlib import Path
+from collections.abc import Generator
+
+import numpy as np
+import simpy
+import pandas as pd
+import polars as pl
+from simpy.events import Process, Timeout
+from simpy.resources.store import FilterStore, FilterStoreGet
+
+from wombat.windfarm import Windfarm
+from wombat.core.mixins import RepairsMixin
+from wombat.core.library import load_yaml
+from wombat.utilities.time import hours_until_future_hour
+from wombat.core.environment import WombatEnvironment
+from wombat.core.data_classes import PortConfig, Maintenance, RepairRequest
+from wombat.core.repair_management import RepairManager
+from wombat.core.service_equipment import ServiceEquipment
+
+
+
+[docs] +class Port(RepairsMixin, FilterStore): + """The offshore wind base port that operates tugboats and performs tow-to-port + repairs. + + .. note:: The operating costs for the port are incorporated into the ``FixedCosts`` + functionality in the high-levl cost bucket: + ``operations_management_administration`` or the more granula cost bucket: + ``marine_management`` + + Parameters + ---------- + env : WombatEnvironment + The simulation environment instance. + windfarm : Windfarm + The simulation windfarm instance. + repair_manager : RepairManager + The simulation repair manager instance. + config : dict | str | Path + A path to a YAML object or dictionary encoding the port's configuration + settings. This will be loaded into a ``PortConfig`` object during + initialization. + + Attributes + ---------- + env : WombatEnvironment + The simulation environment instance. + windfarm : Windfarm + The simulation windfarm instance. + manager : RepairManager + The simulation repair manager instance. + settings : PortConfig + The port's configuration settings, as provided by the user. + requests_serviced : set[str] + The set of requests that have already been serviced to ensure there are no + duplications of labor when splitting out the repair requests to be processed. + turbine_manager : simpy.Resource + A SimPy ``Resource`` object that limits the number of turbines that can be towed + to port, so as not to overload the quayside waters, which is controlled by + ``settings.max_operations``. + crew_manager : simpy.Resource + A SimPy ``Resource`` object that limts the number of repairs that can be + occurring at any given time, which is controlled by ``settings.n_crews``. + service_equipment_manager : simpy.FilterStore + A SimPy ``FilterStore`` object that acts as a coordination system for the + registered tugboats to tow turbines between port and site. In order to tow + in either direction they must be filtered by ``ServiceEquipment.at_port``. This + is generated from the tugboat definitions in ``settings.tugboats``. + active_repairs : dict[str, dict[str, simpy.events.Event]] + A nested dictionary of turbines, and its associated request IDs with a SimPy + ``Event``. The use of events allows them to automatically succeed at the end of + repairs, and once all repairs are processed on a turbine, the tow-to-site + process can commence. + """ + + def __init__( + self, + env: WombatEnvironment, + windfarm: Windfarm, + repair_manager: RepairManager, + config: dict | str | Path, + ) -> None: + super().__init__(env, np.inf) + + self.env = env + self.windfarm = windfarm + self.manager = repair_manager + self.system_request_map: dict[str, list[RepairRequest]] = {} + self.requests_serviced: set[str] = set() + self.invalid_systems: list[str] = [] + + self.manager._register_port(self) + + if not isinstance(config, dict): + config = load_yaml(env.data_dir / "project/port", config) + if TYPE_CHECKING: + assert isinstance(config, dict) + self.settings = PortConfig.from_dict(config) + + self._check_working_hours(which="env") + self.settings.set_non_operational_dates( + self.env.non_operational_start, + self.env.start_year, + self.env.non_operational_end, + self.env.end_year, + ) + self.settings.set_reduced_speed_parameters( + self.env.reduced_speed_start, + self.env.start_year, + self.env.reduced_speed_end, + self.env.end_year, + self.env.reduced_speed, + ) + + # Instantiate the crews, tugboats, and turbine availability + if TYPE_CHECKING: + assert isinstance(self.settings, PortConfig) + self.turbine_manager = simpy.Resource(env, self.settings.max_operations) + self.crew_manager = simpy.Resource(env, self.settings.n_crews) + + service_equipment = [] + for t in self.settings.tugboats: + tugboat = ServiceEquipment(self.env, self.windfarm, repair_manager, t) + tugboat._register_port(self) + service_equipment.append(tugboat) + + self.service_equipment_manager = simpy.FilterStore(env, len(service_equipment)) + self.service_equipment_manager.items = service_equipment + self.active_repairs: dict[str, dict[str, simpy.events.Event]] = {} + self.subassembly_resets: dict[str, list[str]] = {} + + # Create partial functions for the labor and equipment costs for clarity + self.initialize_cost_calculators(which="port") + + # Run the annualized fee logger + if self.settings.annual_fee > 0: + self.env.process(self._log_annual_fee()) + +
+[docs] + def _log_annual_fee(self): + """Logs the annual port lease fee on a monthly-basis.""" + if TYPE_CHECKING: + assert isinstance(self.settings, PortConfig) + monthly_fee = self.settings.annual_fee / 12.0 + ix_month_starts = self.env.weather.filter( + (pl.col("datetime").dt.day() == 1) + & (pl.col("datetime").dt.hour() == 0) + & (pl.col("row_nr") > 0) + ).select(pl.col("row_nr")) + + # At time 0 log the first monthly fee + self.env.log_action( + agent=self.settings.name, + action="monthly lease fee", + reason="port lease", + equipment_cost=monthly_fee, + ) + for i, (ix_month,) in enumerate(ix_month_starts.rows()): + # Get the time to the start of the next month + time_to_next = ( + ix_month + if i == 0 + else ix_month - ix_month_starts.slice(i - 1, 1).item() + ) + + # Log the fee at the start of each month at midnight + yield self.env.timeout(time_to_next) + self.env.log_action( + agent=self.settings.name, + action="monthly lease fee", + reason="port lease", + equipment_cost=monthly_fee, + )
+ + +
+[docs] + def repair_single( + self, request: RepairRequest + ) -> Generator[Timeout | Process, None, None]: + """Simulation logic to process a single repair request. + + Parameters + ---------- + request : RepairRequest + The submitted repair or maintenance request. + """ + # Request a service crew + crew_request = self.crew_manager.request() + yield crew_request + + # Once a crew is available, process the acutal repair + end_shift = self.settings.workday_end + + # Set the default hours to process and remaining hours for the repair + hours_to_process = hours_remaining = request.details.time + + # Create the shared logging among the processes + system = self.windfarm.system(request.system_id) + subassembly = getattr(system, request.subassembly_id) + shared_logging = { + "agent": self.settings.name, + "reason": request.details.description, + "system_id": system.id, + "system_name": system.name, + "part_id": subassembly.id, + "part_name": subassembly.name, + "request_id": request.request_id, + } + + # Continue repairing and waiting a shift until the remaining hours to complete + # the repair is zero + while hours_remaining > 0: + current = self.env.simulation_time + + # Check if the workday is limited by shifts and adjust to stay within shift + if not self.settings.non_stop_shift: + hours_to_process = hours_until_future_hour(current, end_shift) + + # Delay until the next shift if we're at the end + if hours_to_process == 0: + additional = "end of shift; will resume work in the next shift" + yield self.env.process( + self.wait_until_next_shift(additional=additional, **shared_logging) + ) + continue + + # Process the repair for the minimum of remaining hours to completion and + # hours available in the shift + hours_to_process = min(hours_to_process, hours_remaining) + yield self.env.process( + self.process_repair(hours_to_process, request.details, **shared_logging) + ) + + # Decrement the remaining hours and reset the default hours to process back + # to the remaining repair time + hours_remaining -= hours_to_process + hours_to_process = hours_remaining + + # Log the completion of the repair + action = "maintenance" if isinstance(request.details, Maintenance) else "repair" + self.env.log_action( + system_id=system.id, + part_id=subassembly.id, + part_name=subassembly.name, + agent=self.settings.name, + action=f"{action} complete", + reason=request.details.description, + materials_cost=request.details.materials, + additional="complete", + request_id=request.request_id, + ) + + # Make the crew available again + self.crew_manager.release(crew_request) + self.active_repairs[request.system_id][request.request_id].succeed() + yield self.env.process(self.manager.register_repair(request))
+ + +
+[docs] + def transfer_requests_from_manager( + self, system_id: str + ) -> None | list[RepairRequest] | Generator: + """Gets all of a given system's repair requests from the simulation's repair + manager, removes them from that queue, and puts them in the port's queue. + + Parameters + ---------- + system_id : str + The ``System.id`` attribute from the system that will be repaired at port. + + Returns + ------- + None | list[RepairRequest] + The list of repair requests that need to be completed at port. + """ + requests = self.manager.get_all_requests_for_system( + self.settings.name, system_id + ) + if requests is None: + return requests + requests = [r.value for r in requests] # type: ignore + + self.items.extend(requests) + for request in requests: + self.env.log_action( + system_id=request.system_id, + system_name=request.system_name, + part_id=request.subassembly_id, + part_name=request.subassembly_name, + system_ol=float("nan"), + part_ol=float("nan"), + agent=self.settings.name, + action="requests moved to port", + reason="at-port repair can now proceed", + request_id=request.request_id, + ) + _ = yield self.manager.in_process_requests.put(request) + self.active_repairs[system_id][request.request_id] = self.env.event() + request_ids = {el.request_id for el in requests} + self.manager.request_status_map["pending"].difference_update(request_ids) + self.manager.request_status_map["processing"].update(request_ids)
+ + +
+[docs] + def run_repairs(self, system_id: str) -> Generator | None: + """Method that transfers the requests from the repair manager and initiates the + repair sequence. + + Parameters + ---------- + system_id : str + The ``System.id`` that is has been towed to port. + """ + self.active_repairs[system_id] = {} + self.subassembly_resets[system_id] = [] + yield self.env.process(self.transfer_requests_from_manager(system_id)) + + # Get all the requests and run them. + # NOTE: this will all fail if there are somehow no requests, which also means + # something else is completely wrong with the simulation + request_list = self.get_all_requests_for_system(system_id) + for request in request_list: # type: ignore + if TYPE_CHECKING: + assert isinstance(request, FilterStoreGet) + request = request.value + self.requests_serviced.update([request.request_id]) + self.subassembly_resets[system_id].append(request.subassembly_id) + yield self.env.process(self.repair_single(request))
+ + +
+[docs] + def get_all_requests_for_system( + self, system_id: str + ) -> None | Generator[FilterStoreGet, None, None]: + """Gets all repair requests for a specific ``system_id``. + + Parameters + ---------- + system_id : Optional[str], optional + ID of the turbine or OSS; should correspond to ``System.id``. + the first repair requested. + + Returns + ------- + Optional[Generator[FilterStoreGet]] + All repair requests for a given system. If no matching requests are found, + or there aren't any items in the queue yet, then None is returned. + """ + if not self.items: + return None + + # Filter the requests by system + requests = self.items + if system_id is not None: + requests = [el for el in self.items if el.system_id == system_id] + if requests == []: + return None + + # Loop the requests and pop them from the queue + for request in requests: + _ = yield self.get(lambda x: x is request) # pylint: disable=W0640 + + return requests
+ + +
+[docs] + def run_tow_to_port(self, request: RepairRequest) -> Generator[Process, None, None]: + """The method to initiate a tow-to-port repair sequence. + + The process follows the following following routine: + + 1. Request a tugboat from the tugboat resource manager and wait + 2. Runs ``ServiceEquipment.tow_to_port``, which encapsulates the traveling to + site, unmooring, and return tow with a turbine + 3. Transfers the the turbine's repair log to the port, and gets all available + crews to work on repairs immediately + 4. Requests a tugboat to return the turbine to site + 5. Runs ``ServiceEquipment.tow_to_site()``, which encapsulates the tow back to + site, reconnection, resetting the operating status, and returning back to + port + + Parameters + ---------- + request : RepairRequest + The request that initiated the process. This is primarily used for logging + purposes. + + Yields + ------ + Generator[Process, None, None] + The series of events constituting the tow-to-port repairs + """ + # If the request has already been addressed, return + if request.request_id in self.requests_serviced: + return + + system_id = request.system_id + request_id = request.request_id + + # Double check in case a delay causes multiple vessels to be interacting with + # the same turbine + + # Add the requested system to the list of systems undergoing or registered to be + # undergoing repairs, so this method can't be run again on the same system + self.invalid_systems.append(system_id) + + # If the system is already undergoing repairs from other servicing equipment, + # then wait until it's done being serviced + servicing = self.windfarm.system(system_id).servicing + + # Wait for a spot to open up in the port queue + turbine_request = self.turbine_manager.request() + + yield turbine_request & servicing + yield self.env.timeout(self.env.get_random_seconds()) + yield self.windfarm.system(system_id).servicing + + # Request a tugboat to retrieve the turbine + tugboat = yield self.service_equipment_manager.get( + lambda x: x.at_port + and (not x.dispatched) + and "TOW" in x.settings.capability + ) + + # Check that there is enough time to complete towing, connection, and repairs + # before starting the process, otherwise, wait until the next operational period + # TODO: use a more sophisticated guess on timing, other than 20 days + current = self.env.simulation_time.date() + check_range = set( + pd.date_range(current, current + pd.Timedelta(days=20), freq="D").date + ) + intersection = check_range.intersection(self.settings.non_operational_dates_set) + if intersection: + hours_to_next = self.hours_to_next_operational_date( + start_search_date=max(intersection) + ) + self.env.log_action( + agent=self.settings.name, + action="delay", + reason="non-operational period", + additional="waiting for next operational period", + duration=hours_to_next, + ) + yield self.env.timeout(hours_to_next) + + self.requests_serviced.update([request_id]) + + if TYPE_CHECKING: + assert isinstance(tugboat, ServiceEquipment) + yield self.env.process(tugboat.run_tow_to_port(request)) + + # Make the tugboat available again + yield self.service_equipment_manager.put(tugboat) + + # Transfer the repairs to the port queue, which will initiate the repair process + yield self.env.process(self.run_repairs(system_id)) + + # Wait for the repairs to complete + yield simpy.AllOf(self.env, self.active_repairs[system_id].values()) + + # Request a tugboat to tow the turbine back to site, and open the turbine queue + tugboat = yield self.service_equipment_manager.get( + lambda x: x.at_port + and (not x.dispatched) + and "TOW" in x.settings.capability + ) + self.turbine_manager.release(turbine_request) + self.subassembly_resets[system_id] = list( + set(self.subassembly_resets[system_id]) + ) + yield self.env.process( + tugboat.run_tow_to_site(request, self.subassembly_resets[system_id]) + ) + self.invalid_systems.pop(self.invalid_systems.index(system_id)) + + # Make the tugboat available again + yield self.service_equipment_manager.put(tugboat)
+ + +
+[docs] + def run_unscheduled_in_situ( + self, request: RepairRequest, initial: bool = False + ) -> Generator[Process, None, None]: + """Runs the in-situ repair processes for port-based servicing equipment such as + tugboats that will always return back to port, but are not necessarily a feature + of the windfarm itself, such as a crew transfer vessel. + + Parameters + ---------- + request : RepairRequest + The request that triggered the non tow-to-port, but port-based servicing + equipment repair. + + Yields + ------ + Generator[Process, None, None] + The travel and repair processes. + """ + # If the request has already been addressed, return + if request.request_id in self.requests_serviced: + return + + system = self.windfarm.system(request.system_id) + + if initial: + _ = self.manager.get(lambda x: x is request) + self.manager.in_process_requests.put(request) + self.manager.request_status_map["pending"].difference_update( + [request.request_id] + ) + self.manager.request_status_map["processing"].update([request.request_id]) + + # If the system is already undergoing repairs from other servicing equipment, + # then wait until it's done being serviced, then double check + yield system.servicing + seconds_to_wait, *_ = ( + self.env.random_generator.integers(low=0, high=30, size=1) / 3600.0 + ) + yield self.env.timeout(seconds_to_wait) + yield system.servicing + + # Halt the turbine before going further to avoid issue with requests being + # being submitted between now and when the tugboat gets to the turbine + self.requests_serviced.update([request.request_id]) + + # Request a vessel that isn't solely a towing vessel + vessel = yield self.service_equipment_manager.get( + lambda x: x.at_port + and (not x.dispatched) + and x.settings.capability != ["TOW"] + ) + if TYPE_CHECKING: + assert isinstance(vessel, ServiceEquipment) + request = yield self.manager.get(lambda x: x is request) + yield self.env.process(vessel.in_situ_repair(request, initial=True)) + + # If the tugboat finished mid-shift, the in-situ repair logic will keep it + # there, so ensure it returns back to port once it's complete + if not vessel.at_port: + yield self.env.process( + vessel.travel( + start="site", + end="port", + agent=vessel.settings.name, + reason=f"{request.details.description} complete", + system_id=request.system_id, + system_name=request.system_name, + part_id=request.subassembly_id, + part_name=request.subassembly_name, + request_id=request.request_id, + ) + ) + + # Make the tugboat available again + yield self.service_equipment_manager.put(vessel)
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/wombat/core/post_processor.html b/_modules/wombat/core/post_processor.html new file mode 100644 index 00000000..923da7f4 --- /dev/null +++ b/_modules/wombat/core/post_processor.html @@ -0,0 +1,2693 @@ + + + + + + + + + + wombat.core.post_processor — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for wombat.core.post_processor

+"""The postprocessing metric computation."""
+from __future__ import annotations
+
+import warnings
+from copy import deepcopy
+from typing import TYPE_CHECKING, Any
+from pathlib import Path
+from itertools import chain, product
+from collections import Counter
+
+import numpy as np
+import pandas as pd
+
+from wombat.core import FixedCosts
+from wombat.core.library import load_yaml
+
+
+def _check_frequency(frequency: str, which: str = "all") -> str:
+    """Checks the frequency input to ensure it meets the correct criteria according
+    to the ``which`` flag.
+
+    Parameters
+    ----------
+    frequency : str
+        The user-provided value.
+    which : str, optional
+        Designation for which combinations to check for, by default "all".
+        - "all": project, annual, monthly, and month-year
+
+    Returns
+    -------
+    str
+        The lower-case, input with white spaces removed.
+
+    Raises
+    ------
+    ValueError
+        Raised if an invalid value was raised
+    """
+    opts: tuple[str, ...]
+    if which == "all":
+        opts = ("project", "annual", "monthly", "month-year")
+    elif which == "monthly":
+        opts = ("project", "annual", "monthly")
+    elif which == "annual":
+        opts = ("project", "annual")
+    frequency = frequency.lower().strip()
+    if frequency not in opts:
+        raise ValueError(f"``frequency`` must be one of {opts}.")
+    return frequency
+
+
+def _calculate_time_availability(
+    availability: pd.DataFrame,
+    by_turbine: bool = False,
+) -> float | np.ndarray:
+    """Calculates the availability ratio of the whole timeseries or the whole
+    timeseries, by turbine.
+
+    Parameters
+    ----------
+    availability : pd.DataFrame
+        Timeseries array of operating ratios for all turbines.
+    by_turbine : bool, optional
+        If True, calculates the availability rate of each column, otherwise across the
+        whole array, by default False.
+
+    Returns
+    -------
+    float | np.ndarray
+        Availability ratio across the whole timeseries, or broken out by column
+        (turbine).
+    """
+    availability = availability > 0
+    if by_turbine:
+        return availability.values.sum(axis=0) / availability.shape[0]
+    return availability.values.sum() / availability.size
+
+
+
+[docs] +class Metrics: + """The metric computation class for storing logs and compiling results.""" + + _hourly_cost = "hourly_labor_cost" + _salary_cost = "salary_labor_cost" + _labor_cost = "total_labor_cost" + _equipment_cost = "equipment_cost" + _materials_cost = "materials_cost" + _total_cost = "total_cost" + _cost_columns = [ + _hourly_cost, + _salary_cost, + _labor_cost, + _equipment_cost, + _materials_cost, + _total_cost, + ] + + def __init__( + self, + data_dir: str | Path, + events: str | pd.DataFrame, + operations: str | pd.DataFrame, + potential: str | pd.DataFrame, + production: str | pd.DataFrame, + inflation_rate: float, + project_capacity: float, + turbine_capacities: list[float], + substation_id: str | list[str], + turbine_id: str | list[str], + substation_turbine_map: dict[str, dict[str, list[str]]], + service_equipment_names: str | list[str], + fixed_costs: str | None = None, + ) -> None: + """Initializes the Metrics class. + + Parameters + ---------- + data_dir : str | Path + This should be the same as was used for running the analysis. + events : str | pd.DataFrame + Either a pandas ``DataFrame`` or filename to be used to read the csv log + data. + operations : str | pd.DataFrame + Either a pandas ``DataFrame`` or filename to be used to read the csv log + data. + potential : str | pd.DataFrame + Either a pandas ``DataFrame`` or a filename to be used to read the csv + potential power production data. + production : str | pd.DataFrame + Either a pandas ``DataFrame`` or a filename to be used to read the csv power + production data. + inflation_rate : float + The inflation rate to be applied to all dollar amounts from the analysis + starting year to ending year. + project_capacity : float + The project's rated capacity, in MW. + turbine_capacities : Union[float, List[float]] + The capacity of each individual turbine corresponding to ``turbine_id``, in + kW. + substation_id : str | list[str] + The substation id(s). + turbine_id : str | list[str] + The turbine id(s). + substation_turbine_map : dict[str, dict[str, list[str]]] + A copy of ``Windfarm.substation_turbine_map``. This is a dictionary mapping + of the subation IDs (keys) and a nested dictionary of its associated turbine + IDs and each turbine's total plant weighting (turbine capacity / plant + capacity). + service_equipment_names : str | list[str] + The names of the servicing equipment, corresponding to + ``ServiceEquipment.settings.name`` for each ``ServiceEquipment`` in the + simulation. + fixed_costs : str | None + The filename of the project's fixed costs. + """ + self.data_dir = Path(data_dir) + if not self.data_dir.is_dir(): + raise FileNotFoundError(f"{self.data_dir} does not exist") + + self.inflation_rate = 1 + inflation_rate + self.project_capacity = project_capacity + + if fixed_costs is None: + # Create a zero-cost FixedCosts object + self.fixed_costs = FixedCosts.from_dict({"operations": 0}) + else: + if TYPE_CHECKING: + assert isinstance(fixed_costs, str) + fixed_costs = load_yaml(self.data_dir / "project/config", fixed_costs) + if TYPE_CHECKING: + assert isinstance(fixed_costs, dict) + self.fixed_costs = FixedCosts.from_dict(fixed_costs) + + if isinstance(substation_id, str): + substation_id = [substation_id] + self.substation_id = substation_id + + if isinstance(turbine_id, str): + turbine_id = [turbine_id] + self.turbine_id = turbine_id + + self.substation_turbine_map = substation_turbine_map + self.turbine_weights = ( + pd.concat([pd.DataFrame(val) for val in substation_turbine_map.values()]) + .set_index("turbines") + .T + ) + + if isinstance(service_equipment_names, str): + service_equipment_names = [service_equipment_names] + self.service_equipment_names = sorted(set(service_equipment_names)) + + if isinstance(turbine_capacities, (float, int)): + turbine_capacities = [turbine_capacities] + self.turbine_capacities = turbine_capacities + + if isinstance(events, str): + events = self._read_data(events) + self.events = self._apply_inflation_rate(self._tidy_data(events)) + + if isinstance(operations, str): + operations = self._read_data(operations) + self.operations = self._tidy_data(operations) + + if isinstance(potential, str): + potential = self._read_data(potential) + self.potential = self._tidy_data(potential) + + if isinstance(production, str): + production = self._read_data(production) + self.production = self._tidy_data(production) + +
+[docs] + @classmethod + def from_simulation_outputs(cls, fpath: Path | str, fname: str) -> Metrics: + """Creates the Metrics class from the saved outputs of a simulation for ease of + revisiting the calculated metrics. + + Parameters + ---------- + fpath : Path | str + The full path to the file where the data was saved. + fname : Path | str + The filename for where the data was saved, which should be a direct + dictionary mapping for the Metrics initialization. + + Returns + ------- + Metrics + The class object. + """ + data = load_yaml(fpath, fname) + metrics = cls(**data) + return metrics
+ + +
+[docs] + def _tidy_data(self, data: pd.DataFrame) -> pd.DataFrame: + """Tidies the "raw" csv-converted data to be able to be used among the + ``Metrics`` class. + + Parameters + ---------- + data : pd.DataFrame + The csv log data. + + Returns + ------- + pd.DataFrame + A tidied data frame to be used for all the operations in this class. + """ + # Ignore odd pandas casting error for pandas>=1.5(?) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + data = data = data.convert_dtypes() + + if data.index.name != "datetime": + try: + data.datetime = pd.to_datetime(data.datetime) + except AttributeError: + data["datetime"] = pd.to_datetime(data.env_datetime) + data.index = data.datetime + data = data.drop(labels="datetime", axis=1) + data.env_datetime = pd.to_datetime(data.env_datetime) + data = data.assign( + year=data.env_datetime.dt.year, + month=data.env_datetime.dt.month, + day=data.env_datetime.dt.day, + ) + return data
+ + +
+[docs] + def _read_data(self, fname: str) -> pd.DataFrame: + """Reads the csv log data from library. This is intended to be used for the + events or operations data. + + Parameters + ---------- + path : str + Path to the simulation library. + fname : str + Filename of the csv data. + + Returns + ------- + pd.DataFrame + Dataframe of either the events or operations data. + """ + if "events" in fname: + data = ( + pd.read_csv( + self.data_dir / "outputs" / "logs" / fname, + delimiter="|", + engine="pyarrow", + dtype={ + "agent": "string", + "action": "string", + "reason": "string", + "additional": "string", + "system_id": "string", + "system_name": "string", + "part_id": "string", + "part_name": "string", + "request_id": "string", + "location": "string", + }, + ) + .set_index("datetime") + .sort_index() + ) + return data + + data = pd.read_csv( + self.data_dir / "outputs" / "logs" / fname, + delimiter="|", + engine="pyarrow", + ) + return data
+ + +
+[docs] + def _apply_inflation_rate(self, events: pd.DataFrame) -> pd.DataFrame: + """Adjusts the cost data for compounding inflation. + + Parameters + ---------- + inflation_rate : float + The inflation rate to be applied for each year. + events : pd.DataFrame + The events dataframe containing the project cost data. + + Returns + ------- + pd.DataFrame + The events dataframe with costs adjusted for inflation. + """ + adjusted_inflation = deepcopy(self.inflation_rate) + years = events.year.unique() + years.sort() + for year in years: + row_filter = events.year == year + if year > years[0]: + events.loc[row_filter, self._cost_columns] *= adjusted_inflation + adjusted_inflation *= self.inflation_rate + + return events
+ + +
+[docs] + def time_based_availability(self, frequency: str, by: str) -> pd.DataFrame: + """Calculates the time-based availabiliy over a project's lifetime as a single + value, annual average, or monthly average for the whole windfarm or by turbine. + + .. note:: This currently assumes that if there are multiple substations, that + the turbines are all connected to multiple. + + Parameters + ---------- + frequency : str + One of "project", "annual", "monthly", or "month-year". + by : str + One of "windfarm" or "turbine". + + Returns + ------- + pd.DataFrame + The time-based availability at the desired aggregation level. + """ + frequency = _check_frequency(frequency, which="all") + + by = by.lower().strip() + if by not in ("windfarm", "turbine"): + raise ValueError('``by`` must be one of "windfarm" or "turbine".') + by_turbine = by == "turbine" + + # Determine the operational capacity of each turbine with substation downtime + operations_cols = ["year", "month", "day", "windfarm"] + self.turbine_id + turbine_operations = self.operations[operations_cols].copy() + for sub, val in self.substation_turbine_map.items(): + turbine_operations[val["turbines"]] *= self.operations[[sub]].values + + hourly = turbine_operations.loc[:, self.turbine_id] + + # TODO: The below should be better summarized as: + # (availability > 0).groupby().sum() / groupby().count() + + if frequency == "project": + availability = _calculate_time_availability(hourly, by_turbine=by_turbine) + if not by_turbine: + return pd.DataFrame([availability], columns=["windfarm"]) + + if TYPE_CHECKING: + assert isinstance(availability, np.ndarray) + availability = pd.DataFrame( + availability.reshape(1, -1), columns=self.turbine_id + ) + return availability + elif frequency == "annual": + date_time = turbine_operations[["year"]] + counts = turbine_operations.groupby(by="year").count() + counts = counts[self.turbine_id] if by_turbine else counts[["windfarm"]] + annual = [ + _calculate_time_availability( + hourly[date_time.year == year], + by_turbine=by_turbine, + ) + for year in counts.index + ] + return pd.DataFrame(annual, index=counts.index, columns=counts.columns) + elif frequency == "monthly": + date_time = turbine_operations[["month"]] + counts = turbine_operations.groupby(by="month").count() + counts = counts[self.turbine_id] if by_turbine else counts[["windfarm"]] + monthly = [ + _calculate_time_availability( + hourly[date_time.month == month], + by_turbine=by_turbine, + ) + for month in counts.index + ] + return pd.DataFrame(monthly, index=counts.index, columns=counts.columns) + elif frequency == "month-year": + date_time = turbine_operations[["year", "month"]] + counts = turbine_operations.groupby(by=["year", "month"]).count() + counts = counts[self.turbine_id] if by_turbine else counts[["windfarm"]] + month_year = [ + _calculate_time_availability( + hourly[(date_time.year == year) & (date_time.month == month)], + by_turbine=by_turbine, + ) + for year, month in counts.index + ] + return pd.DataFrame(month_year, index=counts.index, columns=counts.columns)
+ + +
+[docs] + def production_based_availability(self, frequency: str, by: str) -> pd.DataFrame: + """Calculates the production-based availabiliy over a project's lifetime as a + single value, annual average, or monthly average for the whole windfarm or by + turbine. + + .. note:: This currently assumes that if there are multiple substations, that + the turbines are all connected to multiple. + + Parameters + ---------- + frequency : str + One of "project", "annual", "monthly", or "month-year". + by : str + One of "windfarm" or "turbine". + + Returns + ------- + pd.DataFrame + The production-based availability at the desired aggregation level. + """ + frequency = _check_frequency(frequency, which="all") + + by = by.lower().strip() + if by not in ("windfarm", "turbine"): + raise ValueError('``by`` must be one of "windfarm" or "turbine".') + by_turbine = by == "turbine" + + if by_turbine: + production = self.production.loc[:, self.turbine_id] + potential = self.potential.loc[:, self.turbine_id] + else: + production = self.production[["windfarm"]].copy() + potential = self.potential[["windfarm"]].copy() + + if frequency == "project": + production = production.values + potential = potential.values + if (potential == 0).sum() > 0: + potential[potential == 0] = 1 + + availability = production.sum(axis=0) / potential.sum(axis=0) + if by_turbine: + return pd.DataFrame([availability], columns=self.turbine_id) + else: + return pd.DataFrame([availability], columns=["windfarm"]) + + production["year"] = production.index.year.values + production["month"] = production.index.month.values + + potential["year"] = potential.index.year.values + potential["month"] = potential.index.month.values + + group_cols = deepcopy(self.turbine_id) if by_turbine else ["windfarm"] + if frequency == "annual": + group_cols.insert(0, "year") + production = production[group_cols].groupby("year").sum() + potential = potential[group_cols].groupby("year").sum() + + elif frequency == "monthly": + group_cols.insert(0, "month") + production = production[group_cols].groupby("month").sum() + potential = potential[group_cols].groupby("month").sum() + + elif frequency == "month-year": + group_cols.insert(0, "year") + group_cols.insert(0, "month") + production = production[group_cols].groupby(["year", "month"]).sum() + potential = potential[group_cols].groupby(["year", "month"]).sum() + + if (potential.values == 0).sum() > 0: + potential.loc[potential.values == 0] = 1 + columns = self.turbine_id + if not by_turbine: + production = production.sum(axis=1) + potential = potential.sum(axis=1) + columns = [by] + return pd.DataFrame(production / potential, columns=columns)
+ + +
+[docs] + def capacity_factor(self, which: str, frequency: str, by: str) -> pd.DataFrame: + """Calculates the capacity factor over a project's lifetime as a single value, + annual average, or monthly average for the whole windfarm or by turbine. + + .. note:: This currently assumes that if there are multiple substations, that + the turbines are all connected to multiple. + + + Parameters + ---------- + which : str + One of "net" or "gross". + frequency : str + One of "project", "annual", "monthly", or "month-year". + by : str + One of "windfarm" or "turbine". + + Returns + ------- + pd.DataFrame + The capacity factor at the desired aggregation level. + """ + which = which.lower().strip() + if which not in ("net", "gross"): + raise ValueError('``which`` must be one of "net" or "gross".') + + frequency = _check_frequency(frequency, which="all") + + by = by.lower().strip() + if by not in ("windfarm", "turbine"): + raise ValueError('``by`` must be one of "windfarm" or "turbine".') + by_turbine = by == "turbine" + + production = self.production if which == "net" else self.potential + production = production.loc[:, self.turbine_id] + + if frequency == "project": + if not by_turbine: + potential = production.shape[0] * self.project_capacity * 1000.0 + production = production.values.sum() + return pd.DataFrame([production / potential], columns=["windfarm"]) + + potential = production.shape[0] * np.array(self.turbine_capacities) + return pd.DataFrame(production.sum(axis=0) / potential).T + + production["year"] = production.index.year.values + production["month"] = production.index.month.values + + if frequency == "annual": + group_cols = ["year"] + elif frequency == "monthly": + group_cols = ["month"] + elif frequency == "month-year": + group_cols = ["year", "month"] + + potential = production[group_cols + self.turbine_id].groupby(group_cols).count() + production = production[group_cols + self.turbine_id].groupby(group_cols).sum() + + if by_turbine: + capacity = np.array(self.turbine_capacities, dtype=float) + columns = self.turbine_id + potential *= capacity + else: + capacity = self.project_capacity + production = production.sum(axis=1) + columns = [by] + return pd.DataFrame(production / potential, columns=columns)
+ + +
+[docs] + def task_completion_rate(self, which: str, frequency: str) -> float | pd.DataFrame: + """Calculates the task completion rate (including tasks that are canceled after + a replacement event) over a project's lifetime as a single value, annual + average, or monthly average for the whole windfarm or by turbine. + + Parameters + ---------- + which : str + One of "scheduled", "unscheduled", or "both". + frequency : str + One of "project", "annual", "monthly", or "month-year". + + Returns + ------- + float | pd.DataFrame + The task completion rate at the desired aggregation level. + """ + which = which.lower().strip() + if which not in ("scheduled", "unscheduled", "both"): + raise ValueError( + '``which`` must be one of "scheduled", "unscheduled", or "both".' + ) + + frequency = _check_frequency(frequency, which="all") + + if which == "scheduled": + task_filter = ["maintenance"] + elif which == "unscheduled": + task_filter = ["repair"] + else: + task_filter = ["maintenance", "repair"] + + cols = ["env_datetime", "request_id"] + request_filter = [f"{el} request" for el in task_filter] + completion_filter = [ + f"{task} {el}" for task in task_filter for el in ("complete", "canceled") + ] + requests = self.events.loc[ + self.events.action.isin(request_filter), cols + ].reset_index(drop=True) + completions = self.events.loc[ + self.events.action.isin(completion_filter), cols + ].reset_index(drop=True) + + if frequency == "project": + if requests.shape[0] == 0: + return pd.DataFrame([0.0], columns=["windfarm"]) + return pd.DataFrame( + [completions.shape[0] / requests.shape[0]], columns=["windfarm"] + ) + + requests["year"] = requests.env_datetime.dt.year.values + requests["month"] = requests.env_datetime.dt.month.values + + completions["year"] = completions.env_datetime.dt.year.values + completions["month"] = completions.env_datetime.dt.month.values + + if frequency == "annual": + group_filter = ["year"] + indices = self.operations.year.unique() + elif frequency == "monthly": + group_filter = ["month"] + indices = self.operations.month.unique() + elif frequency == "month-year": + group_filter = ["year", "month"] + indices = ( + self.operations[["year", "month"]] + .groupby(["year", "month"]) + .value_counts() + .index.tolist() + ) + + group_cols = group_filter + ["request_id"] + requests = requests[group_cols].groupby(group_filter).count() + requests.loc[requests.request_id == 0] = 1 + completions = completions[group_cols].groupby(group_filter).count() + + missing = [ix for ix in indices if ix not in requests.index] + requests = pd.concat( + [ + requests, + pd.DataFrame( + np.ones(len(missing)), index=missing, columns=requests.columns + ), + ] + ).sort_index() + + missing = [ix for ix in indices if ix not in completions.index] + completions = pd.concat( + [ + completions, + pd.DataFrame( + np.ones(len(missing)), index=missing, columns=completions.columns + ), + ] + ).sort_index() + + completion_rate = pd.DataFrame(completions / requests) + completion_rate.index = completion_rate.index.set_names(group_filter) + return completion_rate.rename( + columns={"request_id": "Completion Rate", 0: "Completion Rate"} + )
+ + +
+[docs] + def equipment_costs( + self, frequency: str, by_equipment: bool = False + ) -> pd.DataFrame: + """Calculates the equipment costs for the simulation at a project, annual, or + monthly level with (or without) respect to equipment utilized in the simulation. + This excludes any port fees that might apply, which are included in: + ``port_fees``. + + Parameters + ---------- + frequency : str + One of "project", "annual", "monthly", or "month-year". + by_equipment : bool, optional + Indicates whether the values are with resepect to the equipment utilized + (True) or not (False), by default False. + + Returns + ------- + pd.DataFrame + Returns pandas ``DataFrame`` with columns: + - year (if appropriate for frequency) + - month (if appropriate for frequency) + - then any equipment names as they appear in the logs + + Raises + ------ + ValueError + If ``frequency`` is not one of "project", "annual", "monthly", or + "month-year". + ValueError + If ``by_equipment`` is not one of ``True`` or ``False``. + """ + frequency = _check_frequency(frequency, which="all") + + if not isinstance(by_equipment, bool): + raise ValueError("`by_equipment` must be one of `True` or `False`") + + if frequency == "annual": + col_filter = ["year"] + elif frequency == "monthly": + col_filter = ["month"] + elif frequency == "month-year": + col_filter = ["year", "month"] + + cost_col = [self._equipment_cost] + events = self.events.loc[self.events.action != "monthly lease fee"] + if by_equipment: + if frequency == "project": + costs = ( + events.loc[events[self._equipment_cost] > 0, cost_col + ["agent"]] + .groupby(["agent"]) + .sum() + .fillna(0) + .reset_index(level=0) + ) + costs = costs.fillna(costs.max(axis=0)).T + costs = costs.rename(columns=costs.iloc[0]).drop(index="agent") + return costs.reset_index(drop=True) + + col_filter = ["agent"] + col_filter + costs = ( + events.loc[events[self._equipment_cost] > 0, cost_col + col_filter] + .groupby(col_filter) + .sum() + .reset_index(level=0) + ) + costs = pd.concat( + [ + costs[costs.agent == eq][cost_col].rename( + columns={self._equipment_cost: eq} + ) + for eq in costs.agent.unique() + ], + axis=1, + ) + return costs.fillna(value=0) + + if frequency == "project": + return pd.DataFrame([events[cost_col].sum()], columns=cost_col) + + costs = events[cost_col + col_filter].groupby(col_filter).sum() + return costs.fillna(0)
+ + +
+[docs] + def service_equipment_utilization(self, frequency: str) -> pd.DataFrame: + """Calculates the utilization rate for each of the service equipment in the + simulation as the ratio of total number of days each of the servicing + equipment is in operation over the total number of days it's present in the + simulation. This number excludes mobilization time and the time between + visits for scheduled servicing equipment strategies. + + .. note:: For tugboats in a tow-to-port scenario, this ratio will be near + 100% because they are considered to be operating on an as-needed basis per + the port contracting assumptions + + Parameters + ---------- + frequency : str + One of "project" or "annual". + + Returns + ------- + pd.DataFrame + The utilization rate of each of the simulation ``SerivceEquipment``. + + Raises + ------ + ValueError + If ``frequency`` is not one of "project" or "annual". + """ + frequency = _check_frequency(frequency, which="annual") + + operation_days = [] + total_days = [] + operating_actions = [ + "traveling", # traveling between port/site or on-site + "repair", + "maintenance", + "delay", # performing work + "unmooring", + "mooring_reconnection", + "towing", # tugboat classifications + ] + operating_filter = self.events.action.isin(operating_actions) + return_filter = self.events.action == "delay" + return_filter &= ( + (self.events.reason == "work is complete") + & (self.events.additional == "will return next year") + ) | (self.events.reason == "non-operational period") + return_filter &= self.events.additional == "will return next year" + for name in self.service_equipment_names: + equipment_filter = self.events.agent == name + _events = self.events[equipment_filter & operating_filter] + _events = _events.groupby(["year", "month", "day"]).size() + _events = _events.reset_index().groupby("year").count()[["day"]] + operation_days.append(_events.rename(columns={"day": name})) + + ix_filter = equipment_filter & ~return_filter + total = self.events[ix_filter].groupby(["year", "month", "day"]).size() + total = total.reset_index().groupby("year").count()[["day"]] + total_days.append(total.rename(columns={"day": name})) + + operating_df = pd.DataFrame(operation_days[0]) + total_df = pd.DataFrame(total_days[0]) + if len(self.service_equipment_names) > 1: + operating_df = operating_df.join(operation_days[1:], how="outer").fillna(0) + total_df = total_df.join(total_days[1:], how="outer").fillna(1) + + for year in self.events.year.unique(): + if year not in operating_df.index: + missing = pd.DataFrame( + np.zeros((1, operating_df.shape[1])), + index=[year], + columns=operating_df.columns, + ) + operating_df = pd.concat([operating_df, missing], axis=0).sort_index() + if year not in total_df.index: + missing = pd.DataFrame( + np.ones((1, total_df.shape[1])), + index=[year], + columns=operating_df.columns, + ) + total_df = pd.concat([total_df, missing], axis=0).sort_index() + + if frequency == "project": + operating_df = operating_df.reset_index().sum()[ + self.service_equipment_names + ] + total_df = total_df.reset_index().sum()[self.service_equipment_names] + return pd.DataFrame(operating_df / total_df).T + return operating_df / total_df
+ + +
+[docs] + def vessel_crew_hours_at_sea( + self, + frequency: str, + by_equipment: bool = False, + vessel_crew_assumption: dict[str, float] = {}, + ) -> pd.DataFrame: + """Calculates the total number of crew hours at sea that occurred during a + simulation at a project, annual, or monthly level that can be broken out by + servicing equipment. This includes time mobilizing, delayed at sea, servicing, + towing, and traveling. + + .. note:: This metric is intended to be used for offshore wind simulations. + + Parameters + ---------- + frequency : str + One of "project", "annual", "monthly", or "month-year". + by_equipment : bool, optional + Indicates whether the values are with resepect to each tugboat (True) or not + (False), by default False. + vessel_crew_assumption : dict[str, float], optional + Dictionary of vessel names (``ServiceEquipment.settings.name``) and number + of crew members aboard to trannsform the results from vessel hours at sea + to crew hours at sea. + + Returns + ------- + pd.DataFrame + Returns a pandas ``DataFrame`` with columns: + + - year (if appropriate for frequency) + - month (if appropriate for frequency) + - Total Crew Hours at Sea + - {ServiceEquipment.settings.name} (if broken out) + + Raises + ------ + ValueError + If ``frequency`` is not one of "project", "annual", "monthly", or + "month-year". + ValueError + If ``by_equipment`` is not one of ``True`` or ``False``. + ValueError + If ``vessel_crew_assumption`` is not a dictionary. + """ + frequency = _check_frequency(frequency, which="all") + + if not isinstance(by_equipment, bool): + raise ValueError("``by_equipment`` must be one of ``True`` or ``False``") + + if not isinstance(vessel_crew_assumption, dict): + raise ValueError( + "`vessel_crew_assumption` must be a dictionary of vessel name (keys)" + " and number of crew (values)" + ) + + # Filter by the at sea indicators and required columns + at_sea = self.events + at_sea = at_sea.loc[ + at_sea.location.isin(("enroute", "site", "system")) + & at_sea.agent.isin(self.service_equipment_names), + ["agent", "year", "month", "action", "reason", "additional", "duration"], + ].reset_index(drop=True) + + # Create a shell for the final results + total_hours = ( + self.events[["env_time", "year", "month"]] + .groupby(["year", "month"]) + .count() + ) + total_hours = total_hours.reset_index().rename(columns={"env_time": "N"}) + total_hours.N = 0 + + # Apply the vessel crew assumptions + vessels = at_sea.agent.unique() + if vessel_crew_assumption != {}: + for name, n_crew in vessel_crew_assumption.items(): + if name not in vessels: + continue + ix_vessel = at_sea.agent == name + at_sea.loc[ix_vessel, "duration"] *= n_crew + + group_cols = ["agent"] + columns = ["Total Crew Hours at Sea"] + vessels.tolist() + if not by_equipment: + group_cols.pop(0) + columns = ["Total Crew Hours at Sea"] + at_sea = at_sea.groupby(["year", "month"]).sum()[["duration"]].reset_index() + + if frequency == "project": + total_hours = pd.DataFrame([[0]], columns=["duration"]) + if by_equipment: + total_hours = ( + at_sea[["duration", "agent"]] + .groupby(["agent"]) + .sum() + .T.reset_index(drop=True) + ) + total_hours.loc[:, "Total Crew Hours at Sea"] = total_hours.sum().sum() + return total_hours[columns] + else: + return pd.DataFrame(at_sea.sum()[["duration"]]).T.rename( + columns={"duration": "Total Crew Hours at Sea"} + ) + + elif frequency == "annual": + additional_cols = ["year"] + total_hours = total_hours.groupby("year")[["N"]].sum() + elif frequency == "monthly": + additional_cols = ["month"] + total_hours = total_hours.groupby("month")[["N"]].sum() + elif frequency == "month-year": + additional_cols = ["year", "month"] + total_hours = total_hours.groupby(["year", "month"])[["N"]].sum() + + columns = additional_cols + columns + group_cols.extend(additional_cols) + at_sea = at_sea[group_cols + ["duration"]].groupby(group_cols).sum() + if by_equipment: + total = [] + for v in vessels: + total.append(at_sea.loc[v].rename(columns={"duration": v})) + total_hours = total_hours.join( + pd.concat(total, axis=1), how="outer" + ).fillna(0) + total_hours.N = total_hours.sum(axis=1) + total_hours = ( + total_hours.reset_index() + .rename(columns={"N": "Total Crew Hours at Sea"})[columns] + .set_index(additional_cols) + ) + return total_hours + + return at_sea.rename(columns={"duration": "Total Crew Hours at Sea"})
+ + +
+[docs] + def number_of_tows( + self, frequency: str, by_tug: bool = False, by_direction: bool = False + ) -> float | pd.DataFrame: + """Calculates the total number of tows that occurred during a simulation at a + project, annual, or monthly level that can be broken out by tugboat. + + Parameters + ---------- + frequency : str + One of "project", "annual", "monthly", or "month-year". + by_tug : bool, optional + Indicates whether the values are with resepect to each tugboat (True) or not + (False), by default False. + by_direction : bool, optional + Indicates whether the values are with respect to the direction a turbine is + towed (True) or not (False), by default False. + + Returns + ------- + float | pd.DataFrame + Returns either a float for whole project-level costs or a pandas + ``DataFrame`` with columns: + + - year (if appropriate for frequency) + - month (if appropriate for frequency) + - total_tows + - total_tows_to_port (if broken out) + - total_tows_to_site (if broken out) + - {ServiceEquipment.settings.name}_total_tows (if broken out) + - {ServiceEquipment.settings.name}_to_port (if broken out) + - {ServiceEquipment.settings.name}_to_site (if broken out) + + Raises + ------ + ValueError + If ``frequency`` is not one of "project", "annual", "monthly", or + "month-year". + ValueError + If ``by_tug`` is not one of ``True`` or ``False``. + ValueError + If ``by_direction`` is not one of ``True`` or ``False``. + """ + frequency = _check_frequency(frequency, which="all") + + if not isinstance(by_tug, bool): + raise ValueError("``by_tug`` must be one of ``True`` or ``False``") + + if not isinstance(by_direction, bool): + raise ValueError("``by_direction`` must be one of ``True`` or ``False``") + + # Filter out only the towing events + towing = self.events.loc[self.events.action == "towing"].copy() + if towing.shape[0] == 0: + # If this is accessed in an in-situ only scenario, or no tows were activated + # then return back 0 + return pd.DataFrame([[0]], columns=["total_tows"]) + towing.loc[:, "direction"] = "to_site" + ix_to_port = towing.reason == "towing turbine to port" + towing.loc[ix_to_port, "direction"] = "to_port" + + # Get the unique directions and tugboat names + direction_suffix = ("to_port", "to_site") + tugboats = towing.agent.unique().tolist() + + # Create the final column names + columns = ["total_tows"] + if by_direction: + columns.extend([f"{c}_{s}" for c in columns for s in direction_suffix]) + + if by_tug: + tug_columns = [f"{t}_total_tows" for t in tugboats] + if by_direction: + _columns = [f"{t}_{s}" for t in tugboats for s in direction_suffix] + tug_columns.extend(_columns) + tug_columns.sort() + columns.extend(tug_columns) + + # Count the total number of tows by each possibly category + n_tows = towing.groupby(["agent", "year", "month", "direction"]).count() + n_tows = n_tows.rename(columns={"env_time": "N"})["N"].reset_index() + + # Create a shell for the total tows + total_tows = ( + self.events[["env_time", "year", "month"]] + .groupby(["year", "month"]) + .count()["env_time"] + ) + total_tows = total_tows.reset_index().rename(columns={"env_time": "N"}) + total_tows.N = 0 + + # Create the correct time frequency for the number of tows and shell total + group_cols = ["agent", "direction"] + if frequency == "project": + time_cols = [] + n_tows = n_tows[group_cols + ["N"]].groupby(group_cols).sum() + + # If no further work is required, then return the sum as a 1x1 data frame + if not by_tug and not by_direction: + return pd.DataFrame( + [n_tows.reset_index().N.sum()], columns=["total_tows"] + ) + + total_tows = pd.DataFrame([[0]], columns=["N"]) + elif frequency == "annual": + time_cols = ["year"] + columns = time_cols + columns + group_cols.extend(time_cols) + n_tows = n_tows.groupby(group_cols).sum()[["N"]] + total_tows = ( + total_tows[["year", "N"]].groupby(time_cols).sum().reset_index() + ) + elif frequency == "monthly": + time_cols = ["month"] + columns = time_cols + columns + group_cols.extend(time_cols) + n_tows = n_tows[group_cols + ["N"]].groupby(group_cols).sum() + total_tows = ( + total_tows[["month", "N"]].groupby(time_cols).sum().reset_index() + ) + elif frequency == "month-year": + # Already have month-year by default, so skip the n_tows refinement + time_cols = ["year", "month"] + columns = time_cols + columns + group_cols.extend(time_cols) + n_tows = n_tows.set_index(group_cols, drop=True) + + # Create a list of the columns needed for creating the broken down totals + if frequency == "project": + total_cols = ["N"] + else: + total_cols = total_tows.drop(columns=["N"]).columns.tolist() + + # Sum the number of tows by tugboat, if needed + if by_tug: + tug_sums = [] + for tug in tugboats: + tug_sum = n_tows.loc[tug] + tug_sums.append(tug_sum.rename(columns={"N": tug})) + tug_sums_by_direction = pd.concat(tug_sums, axis=1).fillna(0) + + if frequency == "project": + tug_sums = pd.DataFrame(tug_sums_by_direction.sum()).T + else: + tug_sums = tug_sums_by_direction.reset_index().groupby(total_cols).sum() + if TYPE_CHECKING: + assert isinstance(tug_sums, pd.DataFrame) # mypy checking + tug_sums = tug_sums.rename( + columns={t: f"{t}_total_tows" for t in tug_sums.columns} + ) + if TYPE_CHECKING: + assert isinstance(tug_sums, pd.DataFrame) # mypy checking + total = pd.DataFrame( + tug_sums.sum(axis=1), columns=["total_tows"] + ).reset_index() + else: + if not by_direction: + # Sum the totals, then merge the results with the shell data frame, + # and cleanup the columns + total = n_tows.reset_index().groupby(total_cols).sum().reset_index() + total_tows = total_tows.merge(total, on=total_cols, how="outer") + total_tows = total_tows.fillna(0).rename(columns={"N_y": "total_tows"}) + total_tows = total_tows[columns] + if time_cols: + return total_tows.set_index(time_cols) + return total_tows + else: + total = ( + n_tows.groupby(total_cols) + .sum() + .reset_index() + .rename(columns={"N": "total_tows"}) + ) + + # Create the full total tows data + if frequency == "project": + if "index" in total.columns: + total_tows = total.drop(columns=["index"]) + else: + total_tows = ( + total_tows.merge(total, how="outer").drop(columns=["N"]).fillna(0) + ) + total_tows = total_tows.set_index(total_cols) + + # Get the sums by each direction towed, if needed + if by_direction: + if frequency == "project": + direction_sums = n_tows.reset_index().groupby("direction").sum() + for s in direction_suffix: + total_tows.loc[:, f"total_tows_{s}"] = direction_sums.loc[s, "N"] + else: + direction_sums = ( + n_tows.reset_index().groupby(["direction"] + total_cols).sum() + ) + for s in direction_suffix: + total_tows = total_tows.join( + direction_sums.loc[s].rename(columns={"N": f"total_tows_{s}"}) + ).fillna(0) + + # Add in the tugboat breakdown as needed + if by_tug: + total_tows = total_tows.join(tug_sums, how="outer").fillna(0) + for s in direction_suffix: + if frequency == "project": + _total = pd.DataFrame( + tug_sums_by_direction.loc[s] + ).T.reset_index(drop=True) + else: + _total = tug_sums_by_direction.loc[s] + total_tows = total_tows.join( + _total.rename(columns={t: f"{t}_{s}" for t in tugboats}), + how="outer", + ).fillna(0) + total_tows = total_tows.reset_index()[columns] + if time_cols: + return total_tows.set_index(time_cols) + else: + return total_tows + total_tows = total_tows.assign(N=total_tows.sum(axis=1)) + total_tows = total_tows.rename(columns={"N": "total_tows"}).reset_index()[ + columns + ] + if time_cols: + return total_tows.set_index(time_cols) + return total_tows + + if by_tug: + total_tows = ( + total_tows.join(tug_sums, how="outer").fillna(0).reset_index()[columns] + ) + if time_cols: + return total_tows.set_index(time_cols) + return total_tows + + if time_cols: + return total_tows[columns].set_index(time_cols) + return total_tows[columns]
+ + +
+[docs] + def labor_costs( + self, frequency: str, by_type: bool = False + ) -> float | pd.DataFrame: + """Calculates the labor costs for the simulation at a project, annual, or + monthly level that can be broken out by hourly and salary labor costs. + + Parameters + ---------- + frequency : str + One of "project", "annual", "monthly", or "month-year". + by_type : bool, optional + Indicates whether the values are with resepect to the labor types + (True) or not (False), by default False. + + Returns + ------- + float | pd.DataFrame + Returns either a float for whole project-level costs or a pandas + ``DataFrame`` with columns: + + - year (if appropriate for frequency) + - month (if appropriate for frequency) + - total_labor_cost + - hourly_labor_cost (if broken out) + - salary_labor_cost (if broken out) + + Raises + ------ + ValueError + If ``frequency`` is not one of "project", "annual", "monthly", or + "month-year". + ValueError + If ``by_type`` is not one of ``True`` or ``False``. + """ + frequency = _check_frequency(frequency, which="all") + + if not isinstance(by_type, bool): + raise ValueError("``by_type`` must be one of ``True`` or ``False``") + + labor_cols = [self._hourly_cost, self._salary_cost, self._labor_cost] + if frequency == "project": + costs = pd.DataFrame( + self.events[labor_cols].sum(axis=0).values.reshape(1, -1), + columns=labor_cols, + ) + if not by_type: + return costs[[self._labor_cost]] + return costs + + if frequency == "annual": + group_filter = ["year"] + elif frequency == "monthly": + group_filter = ["month"] + elif frequency == "month-year": + group_filter = ["year", "month"] + + costs = ( + self.events.loc[:, labor_cols + group_filter] + .groupby(group_filter) + .sum() + .fillna(value=0) + ) + if not by_type: + return pd.DataFrame(costs[self._labor_cost]) + return costs
+ + +
+[docs] + def equipment_labor_cost_breakdowns( + self, + frequency: str, + by_category: bool = False, + by_equipment: bool = False, + ) -> pd.DataFrame: + """Calculates the producitivty cost and time breakdowns for the simulation at a + project, annual, or monthly level that can be broken out to include the + equipment and labor components, as well as be broken down by servicing + equipment. + + .. note:: Doesn't produce a value if there's no cost associated with a "reason". + + Parameters + ---------- + frequency : str + One of "project", "annual", "monthly", or "month-year". + by_category : bool, optional + Indicates whether to include the equipment and labor categories (True) or + not (False), by default False. + by_equipment : bool, optional + Indicates whether the values are with resepect to the equipment utilized + (True) or not (False), by default False. + + Returns + ------- + pd.DataFrame + Returns pandas ``DataFrame`` with columns: + - year (if appropriate for frequency) + - month (if appropriate for frequency) + - reason + - hourly_labor_cost (if by_category == ``True``) + - salary_labor_cost (if by_category == ``True``) + - total_labor_cost (if by_category == ``True``) + - equipment_cost (if by_category == ``True``) + - total_cost (if broken out) + - total_hours + + Raises + ------ + ValueError + If ``frequency`` is not one of "project", "annual", "monthly", or + "month-year". + ValueError + If ``by_category`` is not one of ``True`` or ``False``. + """ + frequency = _check_frequency(frequency, which="all") + if not isinstance(by_category, bool): + raise ValueError("``by_category`` must be one of ``True`` or ``False``") + if not isinstance(by_equipment, bool): + raise ValueError("``by_equipment`` must be one of ``True`` or ``False``") + + group_filter = ["action", "reason", "additional"] + if by_equipment: + group_filter.insert(0, "agent") + if frequency in ("annual", "month-year"): + group_filter.insert(0, "year") + elif frequency == "monthly": + group_filter.insert(0, "month") + if frequency == "month-year": + group_filter.insert(1, "month") + + action_list = [ + "delay", + "repair", + "maintenance", + "mobilization", + "transferring crew", + "traveling", + "towing", + ] + equipment = self.events[self.events[self._equipment_cost] > 0].agent.unique() + costs = ( + self.events.loc[ + self.events.agent.isin(equipment) + & self.events.action.isin(action_list) + & ~self.events.additional.isin(["work is complete"]), + group_filter + self._cost_columns + ["duration"], + ] + .groupby(group_filter) + .sum() + .reset_index() + .rename(columns={"duration": "total_hours"}) + ) + costs["display_reason"] = [""] * costs.shape[0] + + non_shift_hours = ( + "not in working hours", + "work shift has ended; waiting for next shift to start", + "no more return visits will be made", + "will return next year", + "waiting for next operational period", + "end of shift; will resume work in the next shift", + ) + weather_hours = ( + "weather delay", + "weather unsuitable to transfer crew", + "insufficient time to complete travel before end of the shift", + "weather unsuitable for mooring reconnection", + "weather unsuitable for unmooring", + ) + costs.loc[ + (costs.action == "delay") & (costs.additional.isin(non_shift_hours)), + "display_reason", + ] = "Not in Shift" + costs.loc[costs.action == "repair", "display_reason"] = "Repair" + costs.loc[costs.action == "maintenance", "display_reason"] = "Maintenance" + costs.loc[ + costs.action == "transferring crew", "display_reason" + ] = "Crew Transfer" + costs.loc[costs.action == "traveling", "display_reason"] = "Site Travel" + costs.loc[costs.action == "towing", "display_reason"] = "Towing" + costs.loc[costs.action == "mobilization", "display_reason"] = "Mobilization" + costs.loc[ + costs.additional.isin(weather_hours), "display_reason" + ] = "Weather Delay" + costs.loc[costs.reason == "no requests", "display_reason"] = "No Requests" + + costs.reason = costs.display_reason + + drop_columns = [self._materials_cost, "display_reason", "additional", "action"] + if not by_category: + drop_columns.extend( + [ + self._hourly_cost, + self._salary_cost, + self._labor_cost, + self._equipment_cost, + ] + ) + group_filter.pop(group_filter.index("additional")) + group_filter.pop(group_filter.index("action")) + costs = costs.drop(columns=drop_columns) + costs = costs.groupby(group_filter).sum().reset_index() + + comparison_values: product[tuple[Any, Any]] | product[tuple[Any, Any, Any]] + month_year = frequency == "month-year" + if frequency in ("annual", "month-year"): + years = costs.year.unique() + reasons = costs.reason.unique() + comparison_values = product(years, reasons) + if month_year: + months = costs.month.unique() + comparison_values = product(years, months, reasons) + + zeros = np.zeros(costs.shape[1] - 2).tolist() + for _year, *_month, _reason in comparison_values: + row_filter = costs.year.values == _year + row = [_year, _reason] + zeros + if month_year: + _month = _month[0] + row_filter &= costs.month.values == _month + row = [_year, _month, _reason] + zeros[:-1] + + row_filter &= costs.reason.values == _reason + if costs.loc[row_filter].size > 0: + continue + costs.loc[costs.shape[0]] = row + elif frequency == "monthly": + months = costs.month.unique() + reasons = costs.reason.unique() + comparison_values = product(months, reasons) + zeros = np.zeros(costs.shape[1] - 2).tolist() + for _month, _reason in comparison_values: + row_filter = costs.month.values == _month + row_filter &= costs.reason.values == _reason + row = [_month, _reason] + zeros + if costs.loc[row_filter].size > 0: + continue + costs.loc[costs.shape[0]] = row + + new_sort = [ + "Maintenance", + "Repair", + "Crew Transfer", + "Site Travel", + "Towing", + "Mobilization", + "Weather Delay", + "No Requests", + "Not in Shift", + ] + costs.reason = pd.Categorical(costs.reason, new_sort) + costs = costs.set_index(group_filter) + sort_order = ["reason"] + if by_equipment: + costs = costs.loc[costs.index.get_level_values("agent").isin(equipment)] + costs.index = costs.index.set_names({"agent": "equipment_name"}) + sort_order = ["equipment_name", "reason"] + if frequency == "project": + return costs.sort_values(by=sort_order) + if frequency == "annual": + sort_order = ["year"] + sort_order + return costs.sort_values(by=sort_order) + if frequency == "monthly": + sort_order = ["month"] + sort_order + return costs.sort_values(by=sort_order) + sort_order = ["year", "month"] + sort_order + return costs.sort_values(by=sort_order)
+ + +
+[docs] + def emissions( + self, + emissions_factors: dict, + maneuvering_factor: float = 0.1, + port_engine_on_factor: float = 0.25, + ) -> pd.DataFrame: + """Calculates the emissions, typically in tons, per hour of operations for + transiting, maneuvering (calculated as a % of transiting), idling at the site + (repairs, crew transfer, weather delays), and idling at port (weather delays), + excluding waiting overnight between shifts. + + Parameters + ---------- + emissions_factors : dict + Dictionary of emissions per hour for "transit", "maneuver", "idle at site", + and "idle at port" for each of the servicing equipment in the simulation. + maneuvering_factor : float, optional + The proportion of transit time that can be attributed to + maneuvering/positioning, by default 0.1. + port_engine_on_factor : float, optional + The proportion of idling at port time that can be attributed to having the + engine on and producing emissions, by default 0.25. + + Returns + ------- + pd.DataFrame + DataFrame of "duration" (hours), "distance_km", and "emissions" (tons) for + each servicing equipment in the simulation for each emissions category. + + Raises + ------ + KeyError + Raised if any of the servicing equipment are missing from the + ``emissions_factors`` dictionary. + KeyError + Raised if any of the emissions categories are missing from each servcing + equipment definition in ``emissions_factors``. + """ + if missing := set(self.service_equipment_names).difference( + [*emissions_factors] + ): + raise KeyError( + f"`emissions_factors` is missing the following keys: {missing}" + ) + + valid_categories = ("transit", "maneuvering", "idle at port", "idle at site") + emissions_categories = list( + chain(*[[*val] for val in emissions_factors.values()]) + ) + emissions_input = Counter(emissions_categories) + if ( + len(set(valid_categories).difference(emissions_input.keys())) > 0 + or len(set(emissions_input.values())) > 1 + ): + raise KeyError( + "Each servicing equipment's emissions factors must have inputs for:" + f"{valid_categories}" + ) + + # Create the agent/duration subset + equipment_usage = ( + self.events.loc[ + self.events.agent.isin(self.service_equipment_names), + ["agent", "action", "reason", "location", "duration", "distance_km"], + ] + .groupby(["agent", "action", "reason", "location"]) + .sum() + .reset_index(drop=False) + ) + equipment_usage = equipment_usage.loc[ + ~( + (equipment_usage.action == "delay") + & equipment_usage.reason.isin(("no requests", "work is complete")) + ) + ] + + # Map each of the locations to new categories and filter out unnecessary ones + conditions = [ + equipment_usage.location.eq("site").astype(bool), + equipment_usage.location.eq("system").astype(bool), + equipment_usage.location.eq("port").astype(bool), + equipment_usage.location.eq("enroute").astype(bool), + ] + values = ["idle at site", "idle at site", "idle at port", "transit"] + equipment_usage = ( + equipment_usage.assign( + category=np.select(conditions, values, default="invalid") + ) + .drop(["action", "reason", "location"], axis=1) + .groupby(["agent", "category"]) + .sum() + .drop("invalid", level="category") + ) + + # Create a new emissions factor DataFrame and mapping + categories = list(set().union(emissions_categories)) + emissions_summary = pd.DataFrame( + [], + index=pd.MultiIndex.from_product( + [[*emissions_factors], categories], names=["agent", "category"] + ), + ) + factors = [ + [(eq, cat), ef] + for eq, d in emissions_factors.items() + for cat, ef in d.items() + ] + emissions_summary.loc[[ix for (ix, _) in factors], "emissions_factors"] = [ + ef for (_, ef) in factors + ] + + # Combine the emissions factors and the calculate the total distribution + equipment_usage = equipment_usage.join(emissions_summary, how="outer").fillna(0) + + # Adjust the transiting time to account for maneuvering + transiting = equipment_usage.index.get_level_values("category") == "transit" + manuevering = ( + equipment_usage.index.get_level_values("category") == "maneuvering" + ) + equipment_usage.loc[manuevering, "duration"] = ( + equipment_usage.loc[transiting, "duration"].values * maneuvering_factor + ) + equipment_usage.loc[transiting, "duration"] = equipment_usage.loc[ + transiting, "duration" + ] * (1 - maneuvering_factor) + + # Adjust the idling at port time to only account for when the engine is on + port = equipment_usage.index.get_level_values("category") == "idle at port" + equipment_usage.loc[port, "duration"] = ( + equipment_usage.loc[transiting, "duration"].values * port_engine_on_factor + ) + + equipment_usage = ( + equipment_usage.fillna(0) + .assign( + emissions=equipment_usage.duration * equipment_usage.emissions_factors + ) + .drop(columns=["emissions_factors"]) + .fillna(0, axis=1) + ) + + return equipment_usage
+ + +
+[docs] + def component_costs( + self, frequency: str, by_category: bool = False, by_action: bool = False + ) -> pd.DataFrame: + """Calculates the component costs for the simulation at a project, annual, or + monthly level that can be broken out by cost categories. This will not sum to + the total cost because it is does not include times where there is no work being + done, but costs are being accrued. + + .. note:: It should be noted that the costs will include costs accrued from both + weather delays and shift-to-shift delays. In the future these will be + disentangled. + + Parameters + ---------- + frequency : str + One of "project", "annual", "monthly", or "month-year". + by_category : bool, optional + Indicates whether the values are with resepect to the various cost + categories (True) or not (False), by default False. + by_action : bool, optional + Indicates whether component costs are going to be further broken out by the + action being performed--repair, maintenance, and delay--(True) or not + (False), by default False. + + Returns + ------- + float | pd.DataFrame + Returns either a float for whole project-level costs or a pandas + ``DataFrame`` with columns: + + - year (if appropriate for frequency) + - month (if appropriate for frequency) + - component + - action (if broken out) + - materials_cost (if broken out) + - total_labor_cost (if broken out) + - equipment_cost (if broken out) + - total_cost + + Raises + ------ + ValueError + If ``frequency`` is not one of "project", "annual", "monthly", or + "month-year". + ValueError + If ``by_category`` is not one of ``True`` or ``False``. + ValueError + If ``by_action`` is not one of ``True`` or ``False``. + """ + frequency = _check_frequency(frequency, which="all") + if not isinstance(by_category, bool): + raise ValueError("``by_equipment`` must be one of ``True`` or ``False``") + if not isinstance(by_action, bool): + raise ValueError("``by_equipment`` must be one of ``True`` or ``False``") + + part_filter = ~self.events.part_id.isna() & ~self.events.part_id.isin([""]) + events = self.events.loc[part_filter].copy() + + # Need to simplify the cable identifiers to exclude the connection information + events.loc[:, "component"] = [el.split("::")[0] for el in events.part_id.values] + + group_filter = [] + if frequency == "annual": + group_filter.extend(["year"]) + elif frequency == "monthly": + group_filter.extend(["month"]) + elif frequency == "month-year": + group_filter.extend(["year", "month"]) + + group_filter.append("component") + cost_cols = ["total_cost"] + if by_category: + cost_cols[0:0] = [ + self._materials_cost, + self._labor_cost, + self._equipment_cost, + ] + + if by_action: + repair_map = { + val: "repair" for val in ("repair request", "repair", "repair_complete") + } + maintenance_map = { + val: "maintenance" + for val in ( + "maintenance request", + "maintenance", + "maintenance_complete", + ) + } + delay_map = {"delay": "delay"} + action_map = {**repair_map, **maintenance_map, **delay_map} + events.action = events.action.map(action_map) + group_filter.append("action") + + month_year = frequency == "month-year" + zeros = np.zeros(len(cost_cols)).tolist() + costs = ( + events[group_filter + cost_cols].groupby(group_filter).sum().reset_index() + ) + if not by_action: + costs.loc[:, "action"] = np.zeros(costs.shape[0]) + cols = costs.columns.to_list() + _ix = cols.index("component") + 1 + cols[_ix:_ix] = ["action"] + cols.pop(-1) + costs = costs.loc[:, cols] + + comparison_values: product[tuple[Any, Any]] | product[ + tuple[Any, Any, Any] + ] | product[tuple[Any, Any, Any, Any]] + if frequency in ("annual", "month-year"): + years = costs.year.unique() + components = costs.component.unique() + actions = costs.action.unique() + comparison_values = product(years, components, actions) + if month_year: + months = costs.month.unique() + comparison_values = product(years, months, components, actions) + + for _year, *_month, _component, _action in comparison_values: + row_filter = costs.year.values == _year + row_filter &= costs.component.values == _component + row_filter &= costs.action.values == _action + row = [_year, _component, _action] + zeros + if month_year: + _month = _month[0] + row_filter &= costs.month.values == _month + row = [_year, _month, _component, _action] + zeros + + if costs.loc[row_filter].size > 0: + continue + costs.loc[costs.shape[0]] = row + elif frequency == "monthly": + months = costs.month.unique() + components = costs.component.unique() + actions = costs.action.unique() + comparison_values = product(months, actions, components) + for _month, _action, _component in comparison_values: + row_filter = costs.month.values == _month + row_filter &= costs.component.values == _component + row_filter &= costs.action.values == _action + row = [_month, _component, _action] + zeros + if costs.loc[row_filter].size > 0: + continue + costs.loc[costs.shape[0]] = row + elif frequency == "project": + components = costs.component.unique() + actions = costs.action.unique() + comparison_values = product(actions, components) + for _action, _component in comparison_values: + row_filter = costs.component.values == _component + row_filter &= costs.action.values == _action + row = [_component, _action] + zeros + if costs.loc[row_filter].size > 0: + continue + costs.loc[costs.shape[0]] = row + sort_cols = group_filter + cost_cols + if group_filter != []: + costs = costs.sort_values(group_filter) + if sort_cols != []: + costs = costs.loc[:, sort_cols] + costs = costs.reset_index(drop=True) + return costs if group_filter == [] else costs.set_index(group_filter)
+ + +
+[docs] + def port_fees(self, frequency: str) -> pd.DataFrame: + """Calculates the port fees for the simulation at a project, annual, or monthly + level. This excludes any equipment or labor costs, which are included in: + ``equipment_costs``. + + Parameters + ---------- + frequency : str + One of "project" or "annual", "monthly", ". + + Returns + ------- + pd.DataFrame + The broken out by time port fees with + + Raises + ------ + ValueError + If ``frequency`` not one of "project" or "annual". + """ + frequency = _check_frequency(frequency, which="all") + + column = "port_fees" + port_fee = self.events.loc[ + self.events.action == "monthly lease fee", + ["year", "month", "equipment_cost"], + ].rename(columns={"equipment_cost": column}) + + if port_fee.shape[0] == 0: + return pd.DataFrame([[0]], columns=[column]) + + if frequency == "project": + return pd.DataFrame([port_fee.sum(axis=0).loc[column]], columns=[column]) + elif frequency == "annual": + return port_fee[["year"] + [column]].groupby(["year"]).sum() + elif frequency == "monthly": + return port_fee[["month"] + [column]].groupby(["month"]).sum() + elif frequency == "month-year": + return ( + port_fee[["year", "month"] + [column]].groupby(["year", "month"]).sum() + )
+ + +
+[docs] + def project_fixed_costs(self, frequency: str, resolution: str) -> pd.DataFrame: + """Calculates the fixed costs of a project at the project and annual frequencies + at a given cost breakdown resolution. + + Parameters + ---------- + frequency : str + One of "project" or "annual", "monthly", ". + resolution : st + One of "low", "medium", or "high", where the values correspond to: + + - low: ``FixedCosts.resolution["low"]``, corresponding to itemized costs. + - medium: ``FixedCosts.resolution["medium"]``, corresponding to the + overarching cost categories. + - high: ``FixedCosts.resolution["high"]``, corresponding to a lump sum. + + These values can also be seen through the ``FixedCosts.hierarchy`` + + Returns + ------- + pd.DataFrame + The project's fixed costs as a sum or annualized with high, medium, and low + resolution as desired. + + Raises + ------ + ValueError + If ``frequency`` not one of "project" or "annual". + ValueError + If ``resolution`` must be one of "low", "medium", or "high". + """ + frequency = _check_frequency(frequency, which="all") + + resolution = resolution.lower().strip() + if resolution not in ("low", "medium", "high"): + raise ValueError( + '``resolution`` must be one of "low", "medium", or "high".' + ) + + # Get the appropriate values and convert to the currency base + keys = self.fixed_costs.resolution[resolution] + vals = ( + np.array([[getattr(self.fixed_costs, key) for key in keys]]) + * self.project_capacity + * 1000 + ) + + total = ( + self.operations[["year", "month", "env_time"]] + .groupby(["year", "month"]) + .count() + ) + total = total.rename(columns={"env_time": "N"}) + total.N = 1.0 + + operation_hours = ( + self.operations[["year", "month", "env_time"]] + .groupby(["year", "month"]) + .count() + ) + operation_hours = operation_hours.rename(columns={"env_time": "N"}) + + costs = pd.DataFrame(total.values * vals, index=total.index, columns=keys) + costs *= operation_hours.values.reshape(-1, 1) / 8760.0 + + adjusted_inflation = np.array( + [self.inflation_rate ** (i // 12) for i in range(costs.shape[0])] + ) + costs *= adjusted_inflation.reshape(-1, 1) + + if frequency == "project": + costs = pd.DataFrame(costs.reset_index(drop=True).sum()).T + elif frequency == "annual": + costs = costs.reset_index().groupby("year").sum().drop(columns=["month"]) + elif frequency == "monthly": + costs = costs.reset_index().groupby("month").sum().drop(columns=["year"]) + + return costs
+ + +
+[docs] + def opex(self, frequency: str, by_category: bool = False) -> pd.DataFrame: + """Calculates the project's OpEx for the simulation at a project, annual, or + monthly level. + + Parameters + ---------- + frequency : str + One of project, annual, monthly, or month-year. + + by_category : bool, optional + Indicates whether the values are with resepect to the various cost + categories (True) or not (False), by default False. + + Returns + ------- + pd.DataFrame + The project's OpEx broken out at the desired time and category resolution. + """ + frequency = _check_frequency(frequency, which="all") + + # Get the materials costs and remove the component-level breakdown + materials = self.component_costs(frequency=frequency, by_category=True) + materials = materials.loc[:, ["materials_cost"]].reset_index() + if frequency == "project": + materials = pd.DataFrame(materials.loc[:, ["materials_cost"]].sum()).T + else: + if frequency == "annual": + group_col = ["year"] + elif frequency == "monthly": + group_col = ["month"] + elif frequency == "month-year": + group_col = ["year", "month"] + materials = ( + materials[group_col + ["materials_cost"]].groupby(group_col).sum() + ) + + # Port fees will produce an 1x1 dataframe if values aren't present, so recreate + # it with the appropriate dimension + port_fees = self.port_fees(frequency=frequency) + if frequency != "project" and port_fees.shape == (1, 1): + port_fees = pd.DataFrame([], columns=["port_fees"], index=materials.index) + port_fees = port_fees.fillna(0) + + # Create a list of data frames for the OpEx components + opex_items = [ + self.project_fixed_costs(frequency=frequency, resolution="low"), + port_fees, + self.equipment_costs(frequency=frequency), + self.labor_costs(frequency=frequency), + materials, + ] + + # Join the data frames and sum along the time axis and return + column = "OpEx" + opex = pd.concat(opex_items, axis=1) + opex.loc[:, column] = opex.sum(axis=1) + if by_category: + return opex + return opex[[column]]
+ + +
+[docs] + def process_times(self) -> pd.DataFrame: + """Calculates the time, in hours, to complete a repair/maintenance request, on + both a request to completion basis, and the actual time to complete the repair. + + Returns + ------- + pd.DataFrame + - category (index): repair/maintenance category + - time_to_completion: total number of hours from the time of request to the + time of completion + - process_time: total number of hours it took for the equipment to complete + - the request. + - downtime: total number of hours where the operations were below 100%. + - N: total number of processes in the category. + """ + events_valid = self.events.loc[self.events.request_id != "na"] + + # Summarize all the requests data + request_df = ( + events_valid[["request_id", "env_time", "duration"]] + .groupby("request_id") + .sum() + .sort_index() + ) + request_df_min = ( + events_valid[["request_id", "env_time", "duration"]] + .groupby("request_id") + .min() + .sort_index() + ) + request_df_max = ( + events_valid[["request_id", "env_time", "duration"]] + .groupby("request_id") + .max() + .sort_index() + ) + + # Summarize all the downtime-specific data for all requests + downtime_df = events_valid.loc[events_valid.system_operating_level < 1][ + ["request_id", "env_time", "duration"] + ] + downtime_df_min = ( + downtime_df[["request_id", "env_time", "duration"]] + .groupby("request_id") + .min() + .sort_index() + ) + downtime_df_max = ( + downtime_df[["request_id", "env_time", "duration"]] + .groupby("request_id") + .max() + .sort_index() + ) + + reason_df = ( + events_valid.drop_duplicates(subset=["request_id"])[ + ["request_id", "reason"] + ] + .set_index("request_id") + .sort_index() + ) + + # Summarize the time to first repair/maintenance activity + submitted_df = ( + events_valid.loc[ + events_valid.action.isin(("repair request", "maintenance request")), + ["request_id", "env_time"], + ] + .set_index("request_id") + .sort_index() + ) + action_df = ( + events_valid.loc[ + events_valid.action.isin(("repair", "maintenance")), + ["request_id", "env_time"], + ] + .groupby("request_id") + .min() + .sort_index() + ) + time_to_repair_df = action_df.subtract(submitted_df, axis="index") + + # Create the timing dataframe + timing = pd.DataFrame([], index=request_df_min.index) + timing = timing.join(reason_df[["reason"]]).rename( + columns={"reason": "category"} + ) + timing = timing.join( + request_df_min[["env_time"]] + .join(request_df_max[["env_time"]], lsuffix="_min", rsuffix="_max") + .diff(axis=1)[["env_time_max"]] + .rename(columns={"env_time_max": "time_to_completion"}) + ) + timing = timing.join(request_df[["duration"]]).rename( + columns={"duration": "process_time"} + ) + timing = timing.join( + downtime_df_min[["env_time"]] + .join(downtime_df_max[["env_time"]], lsuffix="_min", rsuffix="_max") + .diff(axis=1)[["env_time_max"]] + .rename(columns={"env_time_max": "downtime"}) + ) + timing = timing.join( + time_to_repair_df.rename(columns={"env_time": "time_to_start"}) + ) + timing["N"] = 1 + + # Return only the categorically summed data + return timing.groupby("category").sum().sort_index()
+ + +
+[docs] + def power_production( + self, frequency: str, by: str = "windfarm", units: str = "gwh" + ) -> float | pd.DataFrame: + """Calculates the power production for the simulation at a project, annual, or + monthly level that can be broken out by turbine. + + Parameters + ---------- + frequency : str + One of "project", "annual", "monthly", or "month-year". + by : str + One of "windfarm" or "turbine". + units : str + One of "gwh", "mwh", or "kwh". + + Returns + ------- + float | pd.DataFrame + Returns either a float for whole project-level costs or a pandas + ``DataFrame`` with columns: + + - year (if appropriate for frequency) + - month (if appropriate for frequency) + - total_power_production + - <turbine_id>_power_production (if broken out) + + Raises + ------ + ValueError + If ``frequency`` is not one of "project", "annual", "monthly", or + "month-year". + ValueError + If ``by_turbine`` is not one of ``True`` or ``False``. + """ + frequency = _check_frequency(frequency, which="all") + + by = by.lower().strip() + if by not in ("windfarm", "turbine"): + raise ValueError('``by`` must be one of "windfarm" or "turbine".') + by_turbine = by == "turbine" + + if units not in ("gwh", "mwh", "kwh"): + raise ValueError('``units`` must be one of "gwh", "mwh", or "kwh".') + if units == "gwh": + divisor = 1e6 + label = "Project Energy Production (GWh)" + elif units == "mwh": + divisor = 1e3 + label = "Project Energy Production (MWh)" + else: + divisor = 1 + label = "Project Energy Production (kWh)" + + if frequency == "annual": + group_cols = ["year"] + elif frequency == "monthly": + group_cols = ["month"] + elif frequency == "month-year": + group_cols = ["year", "month"] + + col_filter = ["windfarm"] + if by_turbine: + col_filter.extend(self.turbine_id) + + if frequency == "project": + production = self.production[col_filter].sum(axis=0) + production = ( + pd.DataFrame( + production.values.reshape(1, -1), + columns=col_filter, + index=[label], + ) + / divisor + ) + return production + return ( + self.production[group_cols + col_filter].groupby(by=group_cols).sum() + / divisor + )
+ + + # Windfarm Financials + +
+[docs] + def npv( + self, frequency: str, discount_rate: float = 0.025, offtake_price: float = 80 + ) -> pd.DataFrame: + """Calculates the net present value of the windfarm at a project, annual, or + monthly resolution given a base discount rate and offtake price. + + .. note:: This function will be improved over time to incorporate more of the + financial parameter at play, such as PPAs. + + Parameters + ---------- + frequency : str + One of "project", "annual", "monthly", or "month-year". + discount_rate : float, optional + The rate of return that could be earned on alternative investments, by + default 0.025. + offtake_price : float, optional + Price of energy, per MWh, by default 80. + + Returns + ------- + pd.DataFrame + The project net prsent value at the desired time resolution. + """ + frequency = _check_frequency(frequency, which="all") + + # Gather the OpEx, and revenues + expenditures = self.opex("month-year") + production = self.power_production("month-year") + revenue: pd.DataFrame = production / 1000 * offtake_price # MWh + + # Instantiate the NPV with the required calculated data and compute the result + npv = revenue.join(expenditures).rename(columns={"windfarm": "revenue"}) + N = npv.shape[0] + npv.loc[:, "discount"] = np.full(N, 1 + discount_rate) ** np.arange(N) + npv.loc[:, "NPV"] = (npv.revenue.values - npv.OpEx.values) / npv.discount.values + + # Aggregate the results to the required resolution + if frequency == "project": + return pd.DataFrame(npv.reset_index().sum()).T[["NPV"]] + elif frequency == "annual": + return npv.reset_index().groupby("year").sum()[["NPV"]] + elif frequency == "monthly": + return npv.reset_index().groupby("month").sum()[["NPV"]] + return npv[["NPV"]]
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/wombat/core/repair_management.html b/_modules/wombat/core/repair_management.html new file mode 100644 index 00000000..1056bacf --- /dev/null +++ b/_modules/wombat/core/repair_management.html @@ -0,0 +1,1196 @@ + + + + + + + + + + wombat.core.repair_management — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for wombat.core.repair_management

+"""Creates the necessary repair classes."""
+
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+from itertools import chain
+from collections import Counter
+from collections.abc import Generator
+
+import numpy as np
+from simpy.resources.store import FilterStore, FilterStoreGet
+
+from wombat.core import (
+    Failure,
+    Maintenance,
+    StrategyMap,
+    RepairRequest,
+    WombatEnvironment,
+    UnscheduledServiceEquipmentData,
+)
+
+
+if TYPE_CHECKING:
+    from wombat.core import Port, ServiceEquipment
+    from wombat.windfarm import Windfarm
+    from wombat.windfarm.system import Cable, System
+
+
+
+[docs] +class RepairManager(FilterStore): + """Provides a class to manage repair and maintenance tasks. + + Parameters + ---------- + FilterStore : simpy.resources.store.FilterStore + The ``simpy`` class on which RepairManager is based to manage the repair and + maintenance tasks. + env : wombat.core.WombatEnvironment + The simulation environment. + capacity : float + The maximum number of tasks that can be submitted to the manager, by default + ``np.inf``. + + Attributes + ---------- + env : wombat.core.WombatEnvironment + The simulation environment. + windfarm: wombat.windfarm.Windfarm + The simulated windfarm. This is only used for getting the operational capacity. + _current_id : int + The logged and auto-incrememented integer base for the ID generated for each + submitted repair request. + downtime_based_equipment: StrategyMap + The mapping between downtime-based servicing equipment and their capabilities. + request_based_equipment: StrategyMap + The mapping between request-based servicing equipment and their capabilities. + """ + + def __init__(self, env: WombatEnvironment, capacity: float = np.inf) -> None: + super().__init__(env, capacity) + + self.env = env + self._current_id = 0 + self.invalid_systems: list[str] = [] + self.systems_in_tow: list[str] = [] + self.systems_waiting_for_tow: list[str] = [] + + self.downtime_based_equipment = StrategyMap() + self.request_based_equipment = StrategyMap() + self.completed_requests = FilterStore(self.env) + self.in_process_requests = FilterStore(self.env) + self.request_status_map: dict[str, set] = { + "pending": set(), + "processing": set(), + "completed": set(), + } + +
+[docs] + def _update_equipment_map(self, service_equipment: ServiceEquipment) -> None: + """Updates ``equipment_map`` with a provided servicing equipment object.""" + capability = service_equipment.settings.capability + strategy = service_equipment.settings.strategy + + if strategy == "downtime": + mapping = self.downtime_based_equipment + elif strategy == "requests": + mapping = self.request_based_equipment + else: + # Shouldn't be possible to get here! + raise ValueError("Invalid servicing equipment!") + + if TYPE_CHECKING: + assert isinstance( + service_equipment.settings, UnscheduledServiceEquipmentData + ) + strategy_threshold = service_equipment.settings.strategy_threshold + if isinstance(capability, list): + for c in capability: + mapping.update(c, strategy_threshold, service_equipment)
+ + +
+[docs] + def _register_windfarm(self, windfarm: Windfarm) -> None: + """Adds the simulation windfarm to the class attributes.""" + self.windfarm = windfarm
+ + +
+[docs] + def _register_equipment(self, service_equipment: ServiceEquipment) -> None: + """Adds the servicing equipment to the class attributes and adds it to the + capabilities mapping. + """ + self._update_equipment_map(service_equipment)
+ + +
+[docs] + def _register_port(self, port: Port) -> None: + """Registers the port with the repair manager, so that they can communicate as + needed. + + Parameters + ---------- + port : Port + The port where repairs will occur. + """ + self.port = port
+ + +
+[docs] + def _create_request_id(self, request: RepairRequest) -> str: + """Creates a unique ``request_id`` to be logged in the ``request``. + + Parameters + ---------- + request : RepairRequest + The request object. + + Returns + ------- + str + An 11-digit identifier starting with "MNT" for maintenance tasks or "RPR" + for repairs. + + Raises + ------ + ValueError + If the ``request.details`` property is not a ``Failure`` or ``Maintenance`` + object, + then a ValueError will be raised. + """ + if isinstance(request.details, Failure): + prefix = "RPR" + elif isinstance(request.details, Maintenance): + prefix = "MNT" + else: + # Note this is a safety, and shouldn't be able to be reached + raise ValueError("A valid ``RepairRequest`` must be submitted") + + request_id = f"{prefix}{str(self._current_id).zfill(8)}" + self._current_id += 1 + return request_id
+ + +
+[docs] + def _is_request_processing(self, request: RepairRequest) -> bool: + """Checks if a repair is being performed, or has already been completed. + + Parameters + ---------- + request : RepairRequest + The request that is about to be submitted to servicing equipment, but needs + to be double-checked against ongoing processes. + + Returns + ------- + bool + True if the request is ongoing or completed, False, if it's ok to processed + with the operation. + """ + if self.items == []: + return False + + rid = request.request_id + processing = (lambda: rid in self.request_status_map["processing"])() + completed = (lambda: rid in self.request_status_map["completed"])() + return processing or completed
+ + +
+[docs] + def _run_equipment_downtime(self, request: RepairRequest) -> None | Generator: + """Run any equipment that has a pending request where the current windfarm + operating capacity is less than or equal to the servicing equipment's threshold. + + TODO: This methodology needs to better resolve dispatching every equipment + relating to a request vs just the one(s) that are required. Basically, don't + dispatch every available HLV, but just one plus one of every other capability + category that has pending requests + """ + # Add an initial check to help avoid simultaneous dispatching + seconds_to_wait, *_ = ( + self.env.random_generator.integers(low=0, high=10, size=1) / 3600.0 + ) + yield self.env.timeout(seconds_to_wait) + + # Port-based servicing equipment should be handled by the port and does not + # have an operating reduction threshold to meet at this time + if "TOW" in request.details.service_equipment: + if request.system_id not in self.port.invalid_systems: + system = self.windfarm.system(request.system_id) + yield system.servicing_queue & system.servicing + yield self.env.timeout(self.env.get_random_seconds(high=1)) + if request.system_id in self.systems_in_tow: + return + self.invalidate_system(system, tow=True) + yield self.env.process(self.port.run_tow_to_port(request)) + return + + # Wait for the actual system or cable to be available + if request.cable: + yield self.windfarm.cable(request.system_id).servicing + else: + yield self.windfarm.system(request.system_id).servicing + + operating_capacity = self.windfarm.current_availability_wo_servicing + for capability in self.request_map: + equipment_mapping = self.downtime_based_equipment.get_mapping(capability) + for i, equipment in enumerate(equipment_mapping): + if operating_capacity > equipment.strategy_threshold: + continue + + # Avoid simultaneous dispatches by waiting a random number of seconds + seconds_to_wait, *_ = ( + self.env.random_generator.integers(low=0, high=30, size=1) / 3600.0 + ) + yield self.env.timeout(seconds_to_wait) + + equipment_obj = equipment.equipment + if equipment_obj.dispatched: + continue + + # Equipment-based logic does not manage system availability, so + # ensure it's available prior to dispatching, and double check in + # case delays causing a timing collision + if equipment_obj.port_based: + if request.system_id in self.port.invalid_systems: + break + + yield self.windfarm.system(request.system_id).servicing + self.env.process( + self.port.run_unscheduled_in_situ(request, initial=True) + ) + else: + self.env.process(equipment_obj.run_unscheduled_in_situ()) + + # Move the dispatched capability to the end of list to ensure proper + # cycling of available servicing equipment + self.downtime_based_equipment.move_equipment_to_end(capability, i)
+ + +
+[docs] + def _run_equipment_requests(self, request: RepairRequest) -> None | Generator: + """Run the first piece of equipment (if none are onsite) for each equipment + capability category where the number of requests is greater than or equal to the + equipment's threshold. + """ + # Add an initial check to help avoid simultaneous dispatching + yield self.env.timeout(self.env.get_random_seconds(high=30)) + + # Port-based servicing equipment should be handled by the port and does not have + # a requests-based threshold to meet at this time + if "TOW" in request.details.service_equipment: + if request.system_id not in self.port.invalid_systems: + self.systems_waiting_for_tow.append(request.system_id) + system = self.windfarm.system(request.system_id) + yield system.servicing_queue & system.servicing + yield self.env.timeout(self.env.get_random_seconds(high=1)) + if request.system_id in self.systems_in_tow: + return + self.invalidate_system(system, tow=True) + yield self.env.process(self.port.run_tow_to_port(request)) + return + + # Wait for the actual system or cable to be available + if request.cable: + yield self.windfarm.cable(request.system_id).servicing + else: + yield self.windfarm.system(request.system_id).servicing + + dispatched = None + for capability, n_requests in self.request_map.items(): + # For a requests basis, the capability and submitted request must match + if capability not in request.details.service_equipment: + continue + equipment_mapping = self.request_based_equipment.get_mapping(capability) + for i, equipment in enumerate(equipment_mapping): + if n_requests < equipment.strategy_threshold: + continue + + # Avoid simultaneous dispatches by waiting a random number of seconds + yield self.env.timeout(self.env.get_random_seconds(high=30)) + + # Run only the first piece of equipment in the mapping list, but ensure + # that it moves to the back of the line after being used + equipment_obj = equipment.equipment + if equipment_obj.dispatched: + break + + # Either run the repair logic from the port for port-based servicing + # equipment, so that it can self-mangge or dispatch the servicing + # equipment directly, when port is an implicitly modeled aspect + if equipment_obj.port_based: + # Equipment-based logic does not manage system availability, so + # ensure it's available prior to dispatching + if request.system_id in self.port.invalid_systems: + break + + yield self.env.process( + self.port.run_unscheduled_in_situ(request, initial=True) + ) + else: + yield self.env.process(equipment_obj.run_unscheduled_in_situ()) + + # Move the dispatched capability to the end of list to ensure proper + # cycling of available servicing equipment + self.request_based_equipment.move_equipment_to_end(capability, i) + dispatched = capability + break + + yield self.env.timeout(1 / 60) # wait one minute for repair to register + + # Double check the the number of reqeusts is still below the threshold following + # the dispatching of a piece of servicing equipment. This mostly pertains to + # highly frequent request with long repair times and low thresholds. + if ( + dispatched is None + or equipment_obj.port_based + or dispatched not in self.request_map + ): + return + n_requests = self.request_map[dispatched] + threshold_check = [ + n_requests >= eq.strategy_threshold + for eq in self.request_based_equipment.get_mapping(dispatched) + ] + if any(threshold_check): + new_request_check = [ + x + for x in self.items + if dispatched in x.details.service_equipment + and not self._is_request_processing(x) + and x is not request + ] + if new_request_check: + new_request = self.get(lambda x: x == new_request_check[0]).value + yield self.env.process(self._run_equipment_requests(new_request))
+ + +
+[docs] + def register_request(self, request: RepairRequest) -> RepairRequest: + """The method to submit requests to the repair mananger and adds a unique + identifier for logging. + + Parameters + ---------- + request : RepairRequest + The request that needs to be tracked and logged. + + Returns + ------- + RepairRequest + The same request as passed into the method, but with a unique identifier + used for logging. + """ + request_id = self._create_request_id(request) + request.assign_id(request_id) + self.put(request) + self.request_status_map["pending"].update([request.request_id]) + return request
+ + +
+[docs] + def submit_request(self, request: RepairRequest) -> None: + """The method to submit requests to the repair mananger and adds a unique + identifier for logging. + + Parameters + ---------- + request : RepairRequest + The request that needs to be tracked and logged. + + Returns + ------- + RepairRequest + The same request as passed into the method, but with a unique identifier + used for logging. + """ + if self.downtime_based_equipment.is_running: + self.env.process(self._run_equipment_downtime(request)) + if self.request_based_equipment.is_running: + self.env.process(self._run_equipment_requests(request))
+ + +
+[docs] + def get_request_by_system( + self, equipment_capability: list[str], system_id: str | None = None + ) -> FilterStoreGet | None: + """Gets all repair requests for a certain turbine with given a sequence of + ``equipment_capability`` as long as it isn't registered as unable to be + serviced. + + Parameters + ---------- + equipment_capability : list[str] + The capability of the servicing equipment requesting repairs to process. + system_id : Optional[str], optional + ID of the turbine or OSS; should correspond to ``System.id``, by default + None. If None, then it will simply sort the list by ``System.id`` and return + the first repair requested. + + Returns + ------- + Optional[FilterStoreGet] + The first repair request for the focal system or None, if self.items is + empty, or there is no matching system. + """ + if not self.items: + return None + + if system_id in self.invalid_systems: + return None + + equipment_capability = set(equipment_capability) # type: ignore + + # Filter the requests by system + requests = self.items + if system_id is not None: + requests = [el for el in self.items if el.system_id == system_id] + if requests == []: + return None + + # Filter the requests by equipment capability and return the first valid request + if TYPE_CHECKING: + assert isinstance(equipment_capability, set) + for request in requests: + if self._is_request_processing(request): + continue + if request.system_id in self.systems_waiting_for_tow: + continue + if equipment_capability.intersection(request.details.service_equipment): + # If this is the first request for the system, make sure no other + # servicing equipment can access i + self.request_status_map["pending"].difference_update( + [requests[0].request_id] + ) + self.request_status_map["processing"].update([requests[0].request_id]) + return self.get(lambda x: x is requests[0]) + + # There were no matching equipment requirements to match the equipment + # attempting to retrieve its next request + return None
+ + +
+[docs] + def get_request_by_severity( + self, + equipment_capability: list[str] | set[str], + severity_level: int | None = None, + ) -> FilterStoreGet | None: + """Gets the next repair request by ``severity_level``. + + Parameters + ---------- + equipment_capability : list[str] + The capability of the equipment requesting possible repairs to make. + severity_level : int + Severity level of focus, default None. + + Returns + ------- + Optional[FilterStoreGet] + Repair request meeting the required criteria. + """ + if not self.items: + return None + + equipment_capability = set(equipment_capability) + + requests = self.items + if severity_level is not None: + # Capture only the desired severity level, if specified + requests = [ + el for el in self.items if (el.severity_level == severity_level) + ] + if requests == []: + return None + + # Re-order requests by severity (high to low) and the order they were submitted + # Need to ensure that the requests are in submission order in case any get put + # back + requests = sorted(requests, key=lambda x: x.severity_level, reverse=True) + for request in requests: + if self._is_request_processing(request): + continue + if request.system_id in self.systems_waiting_for_tow: + continue + if request.cable: + if not self.windfarm.cable(request.system_id).servicing.triggered: + continue + else: + if not self.windfarm.system(request.system_id).servicing.triggered: + continue + if request.system_id not in self.invalid_systems: + if equipment_capability.intersection(request.details.service_equipment): + self.request_status_map["pending"].difference_update( + [request.request_id] + ) + self.request_status_map["processing"].update([request.request_id]) + return self.get(lambda x: x is request) + + # Ensure None is returned if nothing is found in the loop just as a FilterGet + # would if allowed to oeprate without the above looping to identify multiple + # criteria and acting on the request before it's processed + return None
+ + +
+[docs] + def invalidate_system( + self, system: System | Cable | str, tow: bool = False + ) -> None: + """Disables the ability for servicing equipment to service a specific system, + sets the turbine status to be in servicing, and interrupts all the processes + to turn off operations. + + Parameters + ---------- + system : System | Cable | str + The system, cable, or ``str`` id of one to disable repairs. + tow : bool, optional + Set to True if this is for a tow-to-port request. + """ + if isinstance(system, str): + if "::" in system: + system = self.windfarm.cable(system) + else: + system = self.windfarm.system(system) + + if system.id not in self.invalid_systems and system.servicing.triggered: + system.servicing_queue = self.env.event() + self.invalid_systems.append(system.id) + else: + raise RuntimeError( + f"{self.env.simulation_time} {system.id} already being serviced" + ) + if tow: + self.systems_in_tow.append(system.id) + _ = self.systems_waiting_for_tow.pop( + self.systems_waiting_for_tow.index(system.id) + )
+ + +
+[docs] + def interrupt_system(self, system: System | Cable) -> None: + """Sets the turbine status to be in servicing, and interrupts all the processes + to turn off operations. + + Parameters + ---------- + system_id : str + The system to disable repairs. + """ + if system.servicing.triggered and system.id in self.invalid_systems: + system.servicing = self.env.event() + system.interrupt_all_subassembly_processes() + else: + raise RuntimeError( + f"{self.env.simulation_time} {system.id} already being serviced" + )
+ + +
+[docs] + def register_repair(self, repair: RepairRequest) -> Generator: + """Registers the repair as complete with the repair managiner. + + Parameters + ---------- + repair : RepairRequest + The repair that has been completed. + port : bool, optional + If True, indicates that a port handled the repair, otherwise that a managed + servicing equipment handled the repair, by default False. + + Yields + ------ + Generator + The ``completed_requests.put()`` that registers completion. + """ + self.request_status_map["processing"].difference_update([repair.request_id]) + self.request_status_map["completed"].update([repair.request_id]) + yield self.completed_requests.put(repair) + yield self.in_process_requests.get(lambda x: x is repair)
+ + +
+[docs] + def enable_requests_for_system( + self, system: System | Cable, tow: bool = False + ) -> None: + """Reenables service equipment operations on the provided system. + + Parameters + ---------- + system_id : System | Cable + The ``System`` or ``Cable`` that can be operated on again. + tow : bool, optional + Set to True if this is for a tow-to-port request. + """ + if system.servicing.triggered: + raise RuntimeError( + f"{self.env.simulation_time} Repairs were already completed" + f" at {system.id}" + ) + _ = self.invalid_systems.pop(self.invalid_systems.index(system.id)) + if tow: + _ = self.systems_in_tow.pop(self.systems_in_tow.index(system.id)) + system.servicing.succeed() + system.servicing_queue.succeed() + system.interrupt_all_subassembly_processes()
+ + +
+[docs] + def get_all_requests_for_system( + self, agent: str, system_id: str + ) -> list[RepairRequest] | None | Generator: + """Gets all repair requests for a specific ``system_id``. + + Parameters + ---------- + agent : str + The name of the entity requesting all of a system's repair requests. + system_id : Optional[str], optional + ID of the turbine or OSS; should correspond to ``System.id``. + the first repair requested. + + Returns + ------- + Optional[list[RepairRequest]] + All repair requests for a given system. If no matching requests are found, + or there aren't any items in the queue yet, then None is returned. + """ + if not self.items: + return None + + # Filter the requests by system + requests = self.items + if system_id is not None: + requests = [el for el in self.items if el.system_id == system_id] + if requests == []: + return None + + # Loop the requests and pop them from the queue + for request in requests: + self.env.log_action( + system_id=request.system_id, + system_name=request.system_name, + part_id=request.subassembly_id, + part_name=request.subassembly_name, + system_ol=float("nan"), + part_ol=float("nan"), + agent=agent, + action="requests being moved", + reason="", + request_id=request.request_id, + ) + self.request_status_map["pending"].difference_update([request.request_id]) + self.request_status_map["processing"].update([request.request_id]) + _ = yield self.get(lambda x: x is request) # pylint: disable=W0640 + + return requests
+ + +
+[docs] + def purge_subassembly_requests( + self, system_id: str, subassembly_id: str, exclude: list[str] = [] + ) -> list[RepairRequest] | None: + """Yields all the requests for a system/subassembly combination. This is + intended to be used to remove erroneous requests after a subassembly has been + replaced. + + Parameters + ---------- + system_id : str + Either the ``System.id`` or ``Cable.id``. + subassembly_id : str + Either the ``Subassembly.id`` or the ``Cable.id`` repeated for cables. + exclude : list[str] + A list of ``request_id`` to exclude from the purge. This is a specific use + case for the combined cable system/subassembly, but can be to exclude + certain requests from the purge. + + Yields + ------ + Optional[list[RepairRequest]] + All requests made to the repair manager for the provided system/subassembly + combination. Returns None if self.items is empty or the loop terminates + without finding what it is looking for. + """ + if not self.items: + return None + + requests = [ + request + for request in self.items + if ( + request.system_id == system_id + and request.subassembly_id == subassembly_id + and request.request_id not in exclude + ) + ] + if requests == []: + return None + + for request in requests: + which = "repair" if isinstance(request.details, Failure) else "maintenance" + self.env.log_action( + system_id=request.system_id, + system_name=request.system_name, + part_id=request.subassembly_id, + part_name=request.subassembly_name, + system_ol=float("nan"), + part_ol=float("nan"), + agent="RepairManager", + action=f"{which} canceled", + reason="replacement required", + request_id=request.request_id, + ) + self.request_status_map["pending"].difference_update([request.request_id]) + self.request_status_map["processing"].update([request.request_id]) + _ = self.get(lambda x: x is request) # pylint: disable=W0640 + sid = request.system_id + + # Ensure that if it was reset, and a tow was waiting, that it gets cleared + if sid in self.systems_waiting_for_tow: + if sid not in self.systems_in_tow: + _ = self.systems_waiting_for_tow.pop( + self.systems_waiting_for_tow.index(sid) + ) + + return requests
+ + + @property + def request_map(self) -> dict[str, int]: + """Creates an updated mapping between the servicing equipment capabilities and + the number of requests that fall into each capability category (nonzero values + only). + """ + requests = dict( + Counter( + chain.from_iterable(r.details.service_equipment for r in self.items) + ) + ) + return requests
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/wombat/core/service_equipment.html b/_modules/wombat/core/service_equipment.html new file mode 100644 index 00000000..4316a914 --- /dev/null +++ b/_modules/wombat/core/service_equipment.html @@ -0,0 +1,2461 @@ + + + + + + + + + + wombat.core.service_equipment — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for wombat.core.service_equipment

+"""The servicing equipment module provides a small number of utility functions specific
+to the operations of servicing equipment and the `ServiceEquipment` class that provides
+the repair and transportation logic for scheduled, unscheduled, and unscheduled towing
+servicing equipment.
+"""
+# TODO: NEED A SPECIFIC STARTUP METHOD
+from __future__ import annotations
+
+from copy import deepcopy
+from math import ceil
+from typing import TYPE_CHECKING, Any
+from pathlib import Path
+from datetime import timedelta
+from itertools import zip_longest
+from collections.abc import Generator
+
+import numpy as np
+import pandas as pd
+import polars as pl
+from simpy.events import Event, Process, Timeout
+from pandas.core.indexes.datetimes import DatetimeIndex
+
+from wombat.core import (
+    Maintenance,
+    RepairManager,
+    RepairRequest,
+    WombatEnvironment,
+    ServiceEquipmentData,
+)
+from wombat.windfarm import Windfarm
+from wombat.utilities import HOURS_IN_DAY, cache, hours_until_future_hour
+from wombat.core.mixins import RepairsMixin
+from wombat.core.library import load_yaml
+from wombat.windfarm.system import System
+from wombat.core.data_classes import (
+    UNSCHEDULED_STRATEGIES,
+    ScheduledServiceEquipmentData,
+    UnscheduledServiceEquipmentData,
+)
+from wombat.windfarm.system.cable import Cable
+from wombat.windfarm.system.subassembly import Subassembly
+
+
+if TYPE_CHECKING:
+    from wombat.core import Port
+
+
+
+[docs] +def consecutive_groups(data: np.ndarray, step_size: int = 1) -> list[np.ndarray]: + """Generates the subgroups of an array where the difference between two sequential + elements is equal to the ``step_size``. The intent is to find the length of delays + in a weather forecast. + + Parameters + ---------- + data : np.ndarray + An array of integers. + step_size : int, optional + The step size to be considered a consecutive number, by default 1. + + Returns + ------- + list[np.ndarray] + A list of arrays of the consecutive elements of ``data``. + """ + # Consecutive groups in delay + groups = np.split(data, np.where(np.diff(data) != step_size)[0] + 1) + return groups
+ + + +
+[docs] +def calculate_delay_from_forecast( + forecast: np.ndarray, hours_required: int | float +) -> tuple[bool, int]: + """Calculates the delay from the binary weather forecast for if that hour is all + clear for operations. + + Parameters + ---------- + forecast : np.ndarray + Truth array to indicate if that hour satisfies the weather limit requirements. + hours_required : np.ndarray + The minimum clear weather window required, in hours. + + Returns + ------- + tuple[bool, int] + Indicator if a window is found (``True``) or not (``False``), and the number + of hours the event needs to be delayed in order to start. + """ + safe_operating_windows = consecutive_groups(np.where(forecast)[0]) + window_lengths = np.array([window.size for window in safe_operating_windows]) + clear_windows = np.where(window_lengths >= hours_required)[0] + if clear_windows.size == 0: + return False, forecast.shape[0] + return True, safe_operating_windows[clear_windows[0]][0]
+ + + +
+[docs] +def validate_end_points(start: str, end: str, no_intrasite: bool = False) -> None: + """Checks the starting and ending locations for traveling and towing. + + Parameters + ---------- + start : str + The starting location; should be on of: "site", "system", or "port". + end : str + The ending location; should be on of: "site", "system", or "port". + no_intrasite : bool + A flag to disable intrasite travel, so that ``start`` and ``end`` cannot + both be "system", by default False. + + Raises + ------ + ValueError + Raised if the starting location is invalid. + ValueError + Raised if the ending location is invalid + ValueError + Raised if "system" is provided to both ``start`` and ``end``, but + ``no_intrasite`` is set to ``True``. + """ + if start not in ("port", "site", "system"): + raise ValueError( + "``start`` location must be one of 'port', 'site', or 'system'!" + ) + if end not in ("port", "site", "system"): + raise ValueError("``end`` location must be one of 'port', 'site', or 'system'!") + if no_intrasite and (start == end == "system"): + raise ValueError("No travel within the site is allowed for this process")
+ + + +
+[docs] +def reset_system_operations(system: System, subassembly_resets: list[str]) -> None: + """Completely resets the failure and maintenance events for a given system + and its subassemblies, and puts each ``Subassembly.operating_level`` back to 100%. + + .. note:: This is only intended to be used in conjunction with a tow-to-port + repair where a turbine will be completely serviced. + + Parameters + ---------- + system : System + The turbine to be reset. + subassembly_resets : list[str] + The `subassembly_id`s to reset to good as new, if not assuming all + subassemblies. + """ + for subassembly in system.subassemblies: + if subassembly.name in subassembly_resets: + subassembly.operating_level = 1.0 + subassembly.recreate_processes()
+ + + +
+[docs] +class ServiceEquipment(RepairsMixin): + """Provides a servicing equipment object that can handle various maintenance and + repair tasks. + + Parameters + ---------- + env : WombatEnvironment + The simulation environment. + windfarm : Windfarm + The ``Windfarm`` object. + repair_manager : RepairManager + The ``RepairManager`` object. + equipment_data_file : str + The equipment settings file name with extension. + + Attributes + ---------- + env : WombatEnvironment + The simulation environment instance. + windfarm : Windfarm + The simulation windfarm instance. + manager : RepairManager + The simulation repair manager instance. + settings : ScheduledServiceEquipmentData | UnscheduledServiceEquipmentData + The servicing equipment's configuration settings, as provided by the user. + onsite : bool + Indicates if the servicing equipment is at the site (``True``), or not + (``False``). + enroute : bool + Indicates if the servicing equipment is on its way to the site (``True``), + or not (``False``). + at_port : bool + Indicates if the servicing equipment is at the port, or similar location for + land-based, (``True``), or not (``False``). + at_system : bool + Indications if the servicing equipment is at a cable, substation, or turbine + while on the site (``True``), or not (``False``). + current_system : str | None + Either the ``System.id`` if ``at_system``, or ``None`` if not. + transferring_crew : bool + Indications if the servicing equipment is at a cable, substation, or turbine + and transferring the crew to or from that system (``True``), or not + (``False``). + """ + + def __init__( + self, + env: WombatEnvironment, + windfarm: Windfarm, + repair_manager: RepairManager, + equipment_data_file: str | Path | dict, + ): + """Initializes the ``ServiceEquipment`` class. + + Parameters + ---------- + env : WombatEnvironment + The simulation environment. + windfarm : Windfarm + The ``Windfarm`` object. + repair_manager : RepairManager + The ``RepairManager`` object. + equipment_data_file : str + The equipment settings file name with extension. + """ + self.env = env + self.windfarm = windfarm + self.manager = repair_manager + self.onsite = False # True: mobilized to the site, but not necessarily at_site + self.dispatched = False + self.enroute = False + self.at_port = False + self.at_site = False + self.at_system = False + self.transferring_crew = False + self.current_system = None # type: str | None + self.port_based = False # Changed to False if port is registered + self.settings: ScheduledServiceEquipmentData | UnscheduledServiceEquipmentData + + if isinstance(equipment_data_file, (str, Path)): + data = load_yaml(env.data_dir / "vessels", equipment_data_file) + else: + data = equipment_data_file + + try: + if data["start_year"] < self.env.start_year: + data["start_year"] = self.env.start_year + if data["end_year"] > self.env.end_year: + data["end_year"] = self.env.end_year + except KeyError: + # Ignores for unscheduled maintenace equipment that won't have this input + pass + + # NOTE: mypy is not caught up with attrs yet :( + self.settings = ServiceEquipmentData(data).determine_type() # type: ignore + + # Register servicing equipment with the repair manager if it is using an + # unscheduled maintenance scenario, so it can be dispatched as needed + if self.settings.strategy in UNSCHEDULED_STRATEGIES: + self.manager._register_equipment(self) + + # Only run the equipment if it is on a scheduled basis, otherwise wait + # for it to be dispatched + if self.settings.strategy == "scheduled": + self.env.process(self.run_scheduled_in_situ()) + + # Create partial functions for the labor and equipment costs for clarity + self.initialize_cost_calculators(which="equipment") + +
+[docs] + def finish_setup_with_environment_variables(self) -> None: + """A post-initialization step that will override unset parameters with those + from the the environemt that may have already been set. + """ + self._check_working_hours(which="env") + self.settings._set_port_distance(self.env.port_distance) + self.settings.set_non_operational_dates( + self.env.non_operational_start, + self.env.start_year, + self.env.non_operational_end, + self.env.end_year, + ) + self.settings.set_reduced_speed_parameters( + self.env.reduced_speed_start, + self.env.start_year, + self.env.reduced_speed_end, + self.env.end_year, + self.env.reduced_speed, + )
+ + +
+[docs] + def _register_port(self, port: Port) -> None: + """Method for a tugboat at attach the port for two-way communications. This also + sets the vessel to be at the port, and updates the port_distance. + + Parameters + ---------- + port : Port + The port where the tugboat is based. + """ + self.port = port + self.port_based = True + self.at_port = True + self.settings._set_port_distance(port.settings.site_distance) # type: ignore + + self._check_working_hours(which="port") + + # Set the non-operational start/end dates if needed + self.settings.set_non_operational_dates( + port.settings.non_operational_start, + self.env.start_year, + port.settings.non_operational_end, + self.env.end_year, + ) + + # Set the reduced speed start/end dates if needed + self.settings.set_reduced_speed_parameters( + port.settings.reduced_speed_start, + self.env.start_year, + port.settings.reduced_speed_end, + self.env.end_year, + port.settings.reduced_speed, + )
+ + +
+[docs] + def _set_location(self, end: str, set_current: str | None = None) -> None: + """Keeps track of the servicing equipment by setting the location at either: + site, port, or a specific system. + + Parameters + ---------- + end : str + The ending location; one of "site", or "port" + set_current : str + The ``System.id`` for the new current location, if one is to be set. + """ + if end == "port": + self.at_port = True + self.at_site = False + else: + self.at_port = False + self.at_site = True + self.at_system = True if set_current is not None else False + self.current_system = set_current
+ + +
+[docs] + def _weather_forecast( + self, hours: int | float, which: str + ) -> tuple[pl.Series, pl.Series, pl.Series]: + """Retrieves the weather forecast from the simulation environment, and + translates it to a boolean for satisfactory (True) and unsatisfactory (False) + weather conditions. + + Parameters + ---------- + hours : int | float + The number of hours of weather data that should be retrieved. + which : str + One of "repair" or "transport" to indicate which weather limits to be using. + + Returns + ------- + tuple[pl.Series, pl.Series, pl.Series] + The datetime Series, the hour of day Series, and the boolean Series of + where the weather conditions are within safe operating limits for the + servicing equipment (True) or not (False). + """ + if which == "repair": + max_wind = self.settings.max_windspeed_repair + max_wave = self.settings.max_waveheight_repair + elif which == "transport": + max_wind = self.settings.max_windspeed_transport + max_wave = self.settings.max_waveheight_transport + else: + raise ValueError("`which` must be one of `repair` or `transport`.") + dt, hour, wind, wave = self.env.weather_forecast(hours) + all_clear = (wind <= max_wind) & (wave <= max_wave) + return dt, hour, all_clear
+ + +
+[docs] + def get_speed(self, tow: bool = False) -> float: + """Determines the appropriate speed that the servicing equipment should be + traveling at for towing or traveling, and if the timeframe is during a reduced + speed scenario. + + Parameters + ---------- + tow : bool, optional + True indicates the servicing equipment should be using the towing speed, + and if False, then the traveling speed should be used, by default False. + + Returns + ------- + float + The maximum speed the servicing equipment should be traveling/towing at. + """ + if TYPE_CHECKING: + assert hasattr(self.settings, "reduced_speed_dates_set") + assert hasattr(self.settings, "tow_speed") + speed = self.settings.tow_speed if tow else self.settings.speed + if self.env.simulation_time.date() in self.settings.reduced_speed_dates_set: + if speed > self.settings.reduced_speed: + speed = self.settings.reduced_speed + return speed
+ + +
+[docs] + def get_next_request(self): + """Gets the next request by the rig's method for processing repairs. + + Returns + ------- + simpy.resources.store.FilterStoreGet + The next ``RepairRequest`` to be processed. + """ + # Wait between 2 and 10 seconds to ensure a tow-to-port repair is always first + yield self.env.timeout(self.env.get_random_seconds(low=2)) + + if self.settings.method == "turbine": + request = self.manager.get_request_by_system(self.settings.capability) + if self.settings.method == "severity": + request = self.manager.get_request_by_severity(self.settings.capability) + + if request is None: + yield request + else: + request = request.value + self.manager.invalidate_system(request.system_id) + yield request
+ + +
+[docs] + def enable_string_operations(self, cable: Cable) -> None: + """Traverses the upstream cable and turbine connections and resets the + ``System.cable_failure`` and ``Cable.downstream_failure`` until it hits + another cable failure, then the loop exits. + + Parameters + ---------- + subassembly : Cable + The `Cable` or `System` + """ + farm = self.manager.windfarm + if cable.connection_type == "array": + # If there is another failure downstream of the repaired cable, do nothing + if not cable.downstream_failure.triggered: + return + + # For each upstream turbine and cable, reset their operations + nodes = deepcopy(cable.upstream_nodes) + cables = cable.upstream_cables + tid = nodes.pop(0) + turbine = farm.system(tid) + turbine.cable_failure.succeed() + for tid, cid in zip_longest(nodes, cables, fillvalue=None): # type: ignore + if cid is not None: # type: ignore + cable = farm.cable(cid) + cable.downstream_failure.succeed() + if not cable.broken.triggered: + break + if tid is not None: # only None for last cable on string + turbine = farm.system(tid) + turbine.cable_failure.succeed() + + if cable.connection_type == "export": + # Reset the substation's cable failure + farm.system(cable.end_node).cable_failure.succeed() + + # For each string connected to the substation reset all the turbines and + # cables until another cable failure is encoountered, then move to the next + # string + for t_list, c_list in zip(cable.upstream_nodes, cable.upstream_cables): + for t, c in zip_longest(t_list, c_list, fillvalue=None): + if c is not None: + cable = farm.cable(c) + if not cable.broken.triggered: + break + cable.downstream_failure.succeed() + farm.system(t).cable_failure.succeed()
+ + +
+[docs] + def register_repair_with_subassembly( + self, + subassembly: Subassembly | Cable, + repair: RepairRequest, + starting_operating_level: float, + ) -> None: + """Goes into the repaired subassembly, component, or cable and returns its + ``operating_level`` back to good as new for the specific repair. For fatal cable + failures, all upstream turbines are turned back on unless there is another fatal + cable failure preventing any more from operating. + + Parameters + ---------- + subassembly : Subassembly | Cable + The subassembly or cable that was repaired. + repair : RepairRequest + The request for repair that was submitted. + starting_operating_level : float + The operating level before a repair was started. + """ + operation_reduction = repair.details.operation_reduction + + # Put the subassembly/component back to good as new condition and restart + if repair.details.replacement: + subassembly.operating_level = 1.0 + _ = self.manager.purge_subassembly_requests( + repair.system_id, repair.subassembly_id + ) + subassembly.recreate_processes() + elif operation_reduction == 1: + subassembly.operating_level = starting_operating_level + subassembly.broken.succeed() + elif operation_reduction == 0: + subassembly.operating_level = starting_operating_level + else: + subassembly.operating_level /= 1 - operation_reduction + + if isinstance(subassembly, Subassembly): + self.manager.enable_requests_for_system(subassembly.system) + elif isinstance(subassembly, Cable): + # If the system is a cable, re-enable the upstream systems and cables + self.manager.enable_requests_for_system(subassembly) + if operation_reduction == 1: + self.enable_string_operations(subassembly) + else: + raise TypeError("`subassembly` was neither a `Cable` nor `Subassembly`.") + + self.env.process(self.manager.register_repair(repair))
+ + +
+[docs] + def wait_until_next_operational_period( + self, *, less_mobilization_hours: int = 0 + ) -> Generator[Timeout, None, None]: + """Delays the crew and equipment until the start of the next operational + period. + + TODO: Need a custom error if weather doesn't align with the equipment dates. + + Parameters + ---------- + less_mobilization_hours : int + The number of hours required for mobilization that will be subtracted from + the waiting period to account for mobilization, by default 0. + + Yields + ------ + Generator[Timeout, None, None] + A Timeout event for the number of hours between when the function is called + and when the next operational period starts. + """ + if TYPE_CHECKING: + assert isinstance(self.settings, ScheduledServiceEquipmentData) + current = self.env.simulation_time.date() + ix_match = np.where(current < self.settings.operating_dates)[0] + if ix_match.size > 0: + still_in_operation = True + next_operating_date = self.settings.operating_dates[ix_match[0]] + hours_to_next_shift = ( + self.env.date_ix(next_operating_date) + - self.env.now + + self.settings.workday_start + - less_mobilization_hours + ) + else: + still_in_operation = False + hours_to_next_shift = self.env.max_run_time - self.env.now + + if still_in_operation: + # TODO: This message needs to account for unscheduled equipment as well, but + # the post processor filtering needs to be updated as well + additional = "will return next year" + else: + additional = "no more return visits will be made" + + # Ensures that the statuses are correct + self.at_port = False + self.at_site = False + self.enroute = False + self.onsite = False + + self.env.log_action( + agent=self.settings.name, + action="delay", + reason="work is complete", + additional=additional, + duration=hours_to_next_shift, + location="enroute", + ) + + yield self.env.timeout(hours_to_next_shift)
+ + +
+[docs] + def mobilize_scheduled(self) -> Generator[Timeout, None, None]: + """Mobilizes the ServiceEquipment object by waiting for the next operational + period, less the mobilization time, then logging the mobiliztion cost. + + NOTE: weather delays are not accounted for in this process. + + Yields + ------ + Generator[Timeout, None, None] + A Timeout event for the number of hours between when the function is called + and when the next operational period starts. + """ + mobilization_hours = self.settings.mobilization_days * HOURS_IN_DAY + yield self.env.process( + self.wait_until_next_operational_period( + less_mobilization_hours=mobilization_hours + ) + ) + self.enroute = True + self.env.log_action( + agent=self.settings.name, + action="mobilization", + reason=f"{self.settings.name} is being mobilized", + additional="mobilization", + location="enroute", + duration=mobilization_hours, + ) + + yield self.env.timeout(mobilization_hours) + self.onsite = True + self.enroute = False + + self.env.log_action( + agent=self.settings.name, + action="mobilization", + reason=f"{self.settings.name} has arrived on site", + additional="mobilization", + equipment_cost=self.settings.mobilization_cost, + location="site", + )
+ + +
+[docs] + def mobilize(self) -> Generator[Timeout, None, None]: + """Mobilizes the ServiceEquipment object. + + NOTE: weather delays are not accounted for at this stage. + + Yields + ------ + Generator[Timeout, None, None] + A Timeout event for the number of hours the ServiceEquipment requires for + mobilizing to the windfarm site. + """ + self.enroute = True + mobilization_hours = self.settings.mobilization_days * HOURS_IN_DAY + self.env.log_action( + agent=self.settings.name, + action="mobilization", + reason=f"{self.settings.name} is being mobilized", + additional="mobilization", + duration=mobilization_hours, + location="enroute", + ) + + yield self.env.timeout(mobilization_hours) + self.onsite = True + self.enroute = False + + self.env.log_action( + agent=self.settings.name, + action="mobilization", + reason=f"{self.settings.name} has arrived on site", + additional="mobilization", + equipment_cost=self.settings.mobilization_cost, + location="site", + )
+ + +
+[docs] + def find_uninterrupted_weather_window( + self, hours_required: int | float + ) -> tuple[int | float, bool]: + """Finds the delay required before starting on a process that won't be able to + be interrupted by a weather delay. + + TODO: WEATHER FORECAST NEEDS TO BE DONE FOR ``math.floor(self.now)``, not the + ceiling or there will be a whole lot of rounding up errors on process times. + + Parameters + ---------- + hours_required : int | float + The number of uninterrupted of hours that a process requires for completion. + + Returns + ------- + tuple[int | float, bool] + The number of hours in weather delays before a process can be completed, and + an indicator for if the process has to be delayed until the next shift for + a safe transfer. + """ + # If the hours required for the window is 0, then return 0 and indicate there is + # no shift delay to be processed + if hours_required == 0: + return 0, False + + current = self.env.simulation_time + + # If the time required for a transfer is longer than the time left in the shift, + # then return the hours left in the shift and indicate a shift delay + max_hours = hours_until_future_hour( + self.env.simulation_time, self.settings.workday_end + ) + if hours_required > max_hours: + return max_hours, True + + _, hour, all_clear = self._weather_forecast(max_hours, which="repair") + all_clear &= self._is_workshift(hour) + safe_operating_windows = consecutive_groups(np.where(all_clear)[0]) + window_lengths = np.array([window.size for window in safe_operating_windows]) + + # If all the safe operating windows are shorter than the time required, then + # return the time until the end of the shift and indicate a shift delay + if all(window < hours_required for window in window_lengths): + return max_hours, True + + # Find the length of the delay + delay = safe_operating_windows[ + np.where(window_lengths >= hours_required)[0][0] + ][0] + + # If there is no delay, simply return 0 and the shift delay indicator + if delay == 0: + return delay, False + + # If the delay is non-zero, ensure we get the correct hour difference from now + # until the top of the first available hour + delay = hours_until_future_hour(current, current.hour + delay) + return delay, False
+ + +
+[docs] + def find_interrupted_weather_window( + self, hours_required: int | float + ) -> tuple[DatetimeIndex, np.ndarray, bool]: + """Assesses at which points in the repair window, the wind (and wave) + constraints for safe operation are met. + + The initial search looks for a weather window of length ``hours_required``, and + adds 24 hours to the window for the proceeding 9 days. If no satisfactory window + is found, then the calling process must make another call to this function, but + likely there is something wrong with the constraints or weather conditions if + no window is found within 10 days. + + Parameters + ---------- + hours_required : int | float + The number of hours required to complete the repair. + + Returns + ------- + tuple[DatetimeIndex, np.ndarray, bool] + The pandas DatetimeIndex, and a corresponding boolean array for what points + in the time window are safe to complete a maintenance task or repair. + """ + window_found = False + hours_required = ceil(hours_required) + + for i in range(10): # no more than 10 attempts to find a window + dt_ix, hour_ix, weather = self._weather_forecast( + hours_required + (i * 24), which="repair" + ) + working_hours = self._is_workshift(hour_ix) + window = weather & working_hours + if window.sum() >= hours_required: + window_found = True + break + + return dt_ix, weather, window_found
+ + +
+[docs] + def weather_delay(self, hours: int | float, **kwargs) -> Generator[Event, Any, Any]: + """Processes a weather delay of length ``hours`` hours. If ``hours`` = 0, then + a Timeout is still processed, but not logging is done (this is to increase + consistency and do better typing validation across the codebase). + + Parameters + ---------- + hours : int | float + The lenght, in hours, of the weather delay. + + Yields + ------ + Generator[Event, Any, Any] + If the delay is more than 0 hours, then a ``Timeout`` is yielded of length + ``hours``. + """ + if hours < 0: + raise ValueError( + f"`hours` must be greater than 0 for {self.settings.name} to process" + " a weather delay" + ) + if hours == 0: + return + + salary_cost = self.calculate_salary_cost(hours) + hourly_cost = 0 # contractors not paid for delays + equipment_cost = self.calculate_equipment_cost(hours) + self.env.log_action( + duration=hours, + action="delay", + additional="weather delay", + salary_labor_cost=salary_cost, + hourly_labor_cost=hourly_cost, + equipment_cost=equipment_cost, + **kwargs, + ) + yield self.env.timeout(hours)
+ + +
+[docs] + @cache + def _calculate_intra_site_time( + self, start: str | None, end: str | None + ) -> tuple[float, float]: + """Calculates the time it takes to travel between port and site or between + systems on site. + + Parameters + ---------- + start : str | None + The starting onsite location. If ``None``, then 0 is returned. + end : str | None + The ending onsite location. If ``None``, then 0 is returned. + + Returns + ------- + tuple[float, float] + The travel time and distance between two locations. + """ + distance = 0.0 # setting for invalid cases to have no traveling + valid_sys = self.windfarm.distance_matrix.columns + intra_site = start in valid_sys and end in valid_sys + if intra_site: + distance = self.windfarm.distance_matrix.loc[start, end] + + # if no speed is set, then there is no traveling time + speed = self.get_speed() + travel_time = distance / speed if speed > 0 else 0.0 + + # Infinity is the result of "traveling" between a system twice in a row + if travel_time == float("inf"): + travel_time = 0 + return travel_time, distance
+ + +
+[docs] + def _calculate_uninterrupted_travel_time( + self, distance: float, tow: bool = False + ) -> tuple[float, float]: + """Calculates the delay to the start of traveling and the amount of time it + will take to travel between two locations. + + Parameters + ---------- + distance : float + The distance to be traveled. + tow : bool + Indicates if this travel is for towing (True), or not (False), by default + False. + + Returns + ------- + tuple[float, float] + The length of the delay and the length of travel time, in hours.` -1 is + returned if there no weather windows, and the process will have to be + attempted again. + """ + # Return 0 delay and travel time in the distance is zero + if distance == 0: + return 0, 0 + + speed = self.get_speed(tow=tow) + hours_required = distance / speed + + n = 1 + max_extra_days = 4 + while n <= max_extra_days: # max tries before failing + hours = hours_required + (24 * n) + *_, all_clear = self._weather_forecast(hours, which="repair") + found, delay = calculate_delay_from_forecast(all_clear, hours_required) + if found: + return delay, hours_required + n += 1 + + # Return -1 for delay if no weather window was found + return -1, hours_required
+ + +
+[docs] + def _calculate_interrupted_travel_time( + self, distance: float, tow: bool = False + ) -> float: + """Calculates the travel time with speed reductions for inclement weather, but + without shift interruptions. + + Parameters + ---------- + distance : flaot + The total distance to be traveled, in km. + tow : bool + Indicates if this travel is for towing (True), or not (False), by default + False. + + Returns + ------- + float + _description_ + """ + speed = self.get_speed(tow=tow) + reduction_factor = 1 - self.settings.speed_reduction_factor + reduction_factor = 0.01 if reduction_factor == 0 else reduction_factor + + # get the weather forecast with this time for the max travel time + max_hours = 1 + distance / speed * (1 / reduction_factor) + dt, _, all_clear = self._weather_forecast(max_hours, which="transport") + + # calculate the distance able to be traveled in each 1-hour window + distance_traveled = speed * all_clear.cast(float) + distance_traveled[distance_traveled == 0] = speed * reduction_factor + + # Reduce the first time step by the time lapsed since the start of the hour + # before proceeding + distance_traveled[0] *= 1 - self.env.now % 1 + + # Cumulative sum at the end of each full hour + distance_traveled_sum = distance_traveled.cumsum() + + # Get the index for the end of the hour where the distance requred to be + # traveled is reached. + try: + ix_hours = int(np.where(distance_traveled_sum >= distance)[0][0]) + except IndexError as e: + # If an error occurs because an index maxes out the weather window, check + # that it's not due to having reached the end of the simulation. If so, + # return the max amount of time, but if that's not the case re-raise the + # error. + if self.env.end_datetime in dt: + ix_hours = distance_traveled.shape[0] - 1 + else: + raise e + + # Shave off the extra timing to get the exact travel time + total_hours = ix_hours + 1 # add 1 for 0-indexing + traveled = distance_traveled_sum.take(ix_hours).item() + if traveled > distance: + difference = traveled - distance + speed_at_hour = distance_traveled.take(ix_hours).item() + reduction = difference / speed_at_hour + total_hours -= reduction + + return total_hours
+ + +
+[docs] + def travel( + self, + start: str, + end: str, + set_current: str | None = None, + hours: float | None = None, + distance: float | None = None, + **kwargs, + ) -> Generator[Timeout | Process, None, None]: + """The process for traveling between port and site, or two systems onsite. + + NOTE: This does not currently take the weather conditions into account. + + Parameters + ---------- + start : str + The starting location, one of "site", "port", or "system". + end : str + The starting location, one of "site", "port", or "system". + set_current : str, optional + Where to set ``current_system`` to be if traveling to site or a different + system onsite, by default None. + hours : float, optional + The number hours required for traveling between ``start`` and ``end``. + If provided, no internal travel time will be calculated. + distance : float, optional + The distance, in km, to be traveled. Only used if hours is provided + + Yields + ------ + Generator[Timeout | Process, None, None] + The timeout event for traveling. + """ + validate_end_points(start, end) + if hours is None: + hours = 0.0 + distance = 0.0 + additional = f"traveling from {start} to {end}" + + if start == end == "system": + additional = f"traveling from {self.current_system} to {set_current}" + hours, distance = self._calculate_intra_site_time( + self.current_system, set_current + ) + elif {start, end} == {"site", "port"}: + additional = f"traveling from {start} to {end}" + distance = self.settings.port_distance + try: + hours = self._calculate_interrupted_travel_time(distance) + except IndexError: + # If the end of the simulation is hit while finding a suitable + # window exit, so the simulation can finish. + return None + + if distance is None: + raise ValueError("`distance` must be provided if `hours` is provided.") + + # MyPy helpers + if TYPE_CHECKING: + assert isinstance(distance, (int, float)) + assert isinstance(hours, (float, int)) + + # If the the equipment will arive after the shift is over, then it must travel + # back to port (if needed), and wait for the next shift + if self.settings.non_stop_shift: + hours_to_shift_end = hours + else: + hours_to_shift_end = hours_until_future_hour( + self.env.simulation_time, self.settings.workday_end + ) + future_time = self.env.simulation_time + timedelta(hours=hours) + is_shift = self._is_workshift(future_time.hour) + if ( + (not is_shift or hours > hours_to_shift_end) + and end != "port" + and not self.at_port + ): + kw = { + "additional": "insufficient time to complete travel before end of the shift" # noqa: E501 + } + kw.update(kwargs) # type: ignore + yield self.env.process( + self.travel(start=start, end="port", **kw) # type: ignore + ) + yield self.env.process(self.wait_until_next_shift(**kwargs)) + yield self.env.process( + self.travel(start=start, end=end, set_current=set_current, **kwargs) + ) + return + elif not is_shift and self.at_port: + kw = { + "additional": "insufficient time to complete travel before end of the shift" # noqa: E501 + } + kw.update(kwargs) + yield self.env.process(self.wait_until_next_shift(**kw)) + yield self.env.process( + self.travel(start=start, end=end, set_current=set_current, **kwargs) + ) + return + + salary_cost = self.calculate_salary_cost(hours) + hourly_cost = 0 # contractors not paid for traveling + equipment_cost = self.calculate_equipment_cost(hours) + kwargs.update({"additional": additional}) + self.env.log_action( + action="traveling", + duration=hours, + distance_km=distance, + salary_labor_cost=salary_cost, + hourly_labor_cost=hourly_cost, + equipment_cost=equipment_cost, + location="enroute", + **kwargs, + ) + + # Unregister current_system during travel <- partial fix, still need to figure + # out where current_system needs to be set to None to allow things to "work" + self._set_location("site") + + yield self.env.timeout(hours) + + self._set_location(end, set_current) + where = set_current if set_current is not None else end + kwargs.update({"additional": f"arrived at {where}"}) + self.env.log_action( + action="complete travel", + location=end, + **kwargs, + )
+ + +
+[docs] + def tow( + self, + start: str, + end: str, + set_current: str | None = None, + **kwargs, + ) -> Generator[Timeout | Process, None, None]: + """The process for towing a turbine to/from port. + + Parameters + ---------- + start : str + The starting location; one of "site" or "port". + end : str + The ending location; one of "site" or "port". + set_current : str | None, optional + The ``System.id`` if the turbine is being returned to site, by default None + + Yields + ------ + Generator[Timeout | Process, None, None] + The series of SimPy events that will be processed for the actions to occur. + """ + validate_end_points(start, end, no_intrasite=True) + # Get the distance that needs to be traveled, then calculate the delay and time + # traveling, and log each of them + distance = self.settings.port_distance + delay, hours = self._calculate_uninterrupted_travel_time(distance, tow=True) + if delay == -1: + if start == "site": + kw = deepcopy(kwargs) + kw.update({"reason": "Insufficient weather window, return to port"}) + yield self.env.process(self.travel("site", "port", **kw)) + yield self.env.timeout(HOURS_IN_DAY * 4) + yield self.env.process( + self.tow(start, end, set_current=set_current, **kwargs) + ) + elif start == "port": + kw = deepcopy(kwargs) + kw.update( + {"reason": "Insufficient weather window, will try again later"} + ) + yield self.env.timeout(HOURS_IN_DAY * 4) + yield self.env.process( + self.tow(start, end, set_current=set_current, **kwargs) + ) + + else: + self.env.process(self.weather_delay(delay, location=end, **kwargs)) + + salary_labor_cost = self.calculate_salary_cost(hours) + hourly_labor_cost = self.calculate_hourly_cost(hours) + equipment_cost = self.calculate_equipment_cost(hours) + self.env.log_action( + duration=hours, + distance_km=distance, + action="towing", + salary_labor_cost=salary_labor_cost, + hourly_labor_cost=hourly_labor_cost, + equipment_cost=equipment_cost, + location="enroute", + **kwargs, + ) + + yield self.env.timeout(hours) + self._set_location(end, set_current) + self.env.log_action( + action="complete towing", + salary_labor_cost=salary_labor_cost, + hourly_labor_cost=hourly_labor_cost, + equipment_cost=equipment_cost, + additional="complete", + location=end, + **kwargs, + )
+ + +
+[docs] + def crew_transfer( + self, + system: System | Cable, + subassembly: Subassembly, + request: RepairRequest, + to_system: bool, + ) -> Generator[Timeout | Process, None, None]: + """The process of transfering the crew from the equipment to the ``System`` + for servicing using an uninterrupted weather window to ensure safe transfer. + + Parameters + ---------- + system : System | Cable + The System where the crew needs to be transferred to. + subassembly : Subassembly + The Subassembly that is being worked on. + request : RepairRequest + The repair to be processed. + to_system : bool + True if the crew is being transferred to the system, or False if the crew + is being transferred off the system. + + Returns + ------- + None + None is returned when this is run recursively to not repeat the crew + transfer process. + + Yields + ------ + Generator[Timeout | Process, None, None] + Yields a timeout event for the crew transfer once an uninterrupted weather + window can be found. + """ + hours_to_process = self.settings.crew_transfer_time + salary_cost = self.calculate_salary_cost(hours_to_process) + hourly_cost = self.calculate_hourly_cost(hours_to_process) + equipment_cost = self.calculate_equipment_cost(hours_to_process) + + shared_logging = { + "system_id": system.id, + "system_name": system.name, + "part_id": subassembly.id, + "part_name": subassembly.name, + "system_ol": system.operating_level, + "part_ol": subassembly.operating_level, + "agent": self.settings.name, + "reason": request.details.description, + "request_id": request.request_id, + } + + delay, shift_delay = self.find_uninterrupted_weather_window(hours_to_process) + # If there is a shift delay, then travel to port, wait, and travel back, and + # try again, until no shift delay is required. + while shift_delay: + travel_time = self.env.now + yield self.env.process( + self.travel(start="site", end="port", **shared_logging) + ) + travel_time -= self.env.now # will be negative value, but is flipped + delay -= abs(travel_time) # decrement the delay by the travel time + + # If removing the travel time from the delay puts it past the delay, then + # do not process an additional delay, and simply go to the next step + if delay >= 0: + yield self.env.process( + self.weather_delay(delay, location="port", **shared_logging) + ) + yield self.env.process( + self.wait_until_next_shift( + **shared_logging, + **{"additional": "weather unsuitable to transfer crew"}, + ) + ) + yield self.env.process( + self.travel( + start="port", end="site", set_current=system.id, **shared_logging + ) + ) + delay, shift_delay = self.find_uninterrupted_weather_window( + hours_to_process + ) + + yield self.env.process( + self.weather_delay(delay, location="system", **shared_logging) + ) + + if to_system: + additional = f"transferring crew from {self.settings.name} to {system.id}" + else: + additional = f"transferring crew from {system.id} to {self.settings.name}" + self.env.log_action( + action="transferring crew", + additional=additional, + duration=hours_to_process, + salary_labor_cost=salary_cost, + hourly_labor_cost=hourly_cost, + equipment_cost=equipment_cost, + location="system", + **shared_logging, + ) + self.transferring_crew = True + yield self.env.timeout(hours_to_process) + self.transferring_crew = False + self.env.log_action( + action="complete transfer", + additional="complete", + location="system", + **shared_logging, + )
+ + +
+[docs] + def mooring_connection( + self, + system: System, + request: RepairRequest, + which: str, + ) -> Generator[Timeout | Process, None, None]: + """The process of either umooring a floating turbine to prepare for towing it to + port, or reconnecting it after its repairs have been completed. + + Parameters + ---------- + system : System + The System that needs unmooring/reconnecting. + request : RepairRequest + The repair to be processed. + which : bool + "unmoor" for unmooring the turbine, "reconnect" for reconnecting the + turbine. + + Returns + ------- + None + None is returned when this is run recursively to not repeat the process. + + Yields + ------ + Generator[Timeout | Process, None, None] + Yields a timeout event for the unmooring/reconnection once an uninterrupted + weather window can be found. + """ + if TYPE_CHECKING: + assert isinstance(self.settings, UnscheduledServiceEquipmentData) + which = which.lower().strip() + if which == "unmoor": + hours_to_process = self.settings.unmoor_hours + elif which == "reconnect": + hours_to_process = self.settings.reconnection_hours + else: + raise ValueError( + f"Only `unmoor` and `reconnect` are allowable inputs, not {which}" + ) + + salary_cost = self.calculate_salary_cost(hours_to_process) + hourly_cost = self.calculate_hourly_cost(hours_to_process) + equipment_cost = self.calculate_equipment_cost(hours_to_process) + + shared_logging = { + "system_id": system.id, + "system_name": system.name, + "system_ol": system.operating_level, + "agent": self.settings.name, + "reason": request.details.description, + "request_id": request.request_id, + } + + which_text = "unmooring" if which == "unmoor" else "mooring reconnection" + + delay, shift_delay = self.find_uninterrupted_weather_window(hours_to_process) + # If there is a shift delay, then wait try again. + if shift_delay: + yield self.env.process( + self.weather_delay(delay, location="site", **shared_logging) + ) + yield self.env.process( + self.wait_until_next_shift( + **shared_logging, + **{ + "location": "site", + "additional": f"weather unsuitable for {which_text}", + }, + ) + ) + yield self.env.process( + self.mooring_connection(system, request, which=which) + ) + return + + # If no shift delay, then process any weather delays before dis/connection + yield self.env.process( + self.weather_delay(delay, location="site", **shared_logging) + ) + + if which == "unmoor": + additional = f"Unmooring {system.id} to tow to port" + else: + additional = f"Reconnecting the mooring to {system.id}" + self.env.log_action( + action=which_text, + additional=additional, + duration=hours_to_process, + salary_labor_cost=salary_cost, + hourly_labor_cost=hourly_cost, + equipment_cost=equipment_cost, + location="system", + **shared_logging, # type: ignore + ) + + if which == "unmoor": + yield self.env.timeout(self.settings.unmoor_hours) + else: + yield self.env.timeout(self.settings.reconnection_hours) + + self.env.log_action( + action=f"complete {which_text}", + additional="complete", + **shared_logging, # type: ignore + )
+ + +
+[docs] + def in_situ_repair( + self, + request: RepairRequest, + time_processed: int | float = 0, + prior_operation_level: float = -1.0, + initial: bool = False, + ) -> Generator[Timeout | Process, None, None]: + """Processes the repair including any weather and shift delays. + + Parameters + ---------- + request : RepairRequest + The ``Maintenance`` or ``Failure`` receiving attention. + time_processed : int | float, optional + Time that has already been processed, by default 0. + prior_operation_level : float, optional + The operating level of the ``System`` just before the repair has begun, by + default -1.0. + initial : bool, optional + True for first step in a potentially-recursive logic, otherwise False. When + True, the repair manager will turn off the system being worked on, but if + done multiple times, the simulation will error out. + + Yields + ------ + Generator[Timeout | Process, None, None] + Timeouts for the repair process. + """ + """ + NOTE: THE PROCESS + 1. Travel to site/turbine, if not there already + 2. Transfer Crew + 3. Number of hours required, or hours until the end of the shift + 4. Do repairs for the above + 5. When shift/repair is complete, transfer crew + 6. Travel to next turbine, back to port + + 7. Repeat until the repair is complete + 8. Exit this function + """ + shift_delay = False + + if request.cable: + system = self.windfarm.cable(request.system_id) + cable = request.subassembly_id.split("::")[1:] + subassembly = self.windfarm.graph.edges[cable]["cable"] + else: + system = self.windfarm.system(request.system_id) # type: ignore + subassembly = getattr(system, request.subassembly_id) + + starting_operational_level = max( + prior_operation_level, subassembly.operating_level + ) + shared_logging = { + "system_id": system.id, + "system_name": system.name, + "part_id": subassembly.id, + "part_name": subassembly.name, + "agent": self.settings.name, + "reason": request.details.description, + "request_id": request.request_id, + } + + # Ensure there is enough time to transfer the crew back and forth with a buffer + # of twice the travel time or travel back to port and try again the next shift + start_shift = self.settings.workday_start + end_shift = self.settings.workday_end + current = self.env.simulation_time + hours_required = request.details.time - time_processed + if self.settings.non_stop_shift: + # Default the available to a buffered amount for initial processing + hours_available = hours_required * 2 + else: + hours_available = hours_until_future_hour(current, end_shift) + + if hours_available <= self.settings.crew_transfer_time * 4: + shared_logging.update( + system_ol=system.operating_level, part_ol=subassembly.operating_level + ) + yield self.env.process( + self.travel( + start="site", + end="port", + **shared_logging, + ) + ) + yield self.env.process(self.wait_until_next_shift(**shared_logging)) + + yield self.env.process( + self.in_situ_repair( + request, + prior_operation_level=starting_operational_level, + initial=initial, + ) + ) + return + + # Travel to site or the next system on site + if not self.at_system and self.at_port: + shared_logging.update( + system_ol=system.operating_level, part_ol=subassembly.operating_level + ) + yield self.env.process( + self.travel( + start="port", end="site", set_current=system.id, **shared_logging + ) + ) + elif self.at_system is not None and not self.at_port: + shared_logging.update( + system_ol=system.operating_level, part_ol=subassembly.operating_level + ) + yield self.env.process( + self.travel( + start="system", + end="system", + set_current=system.id, + **shared_logging, + ) + ) + else: + raise RuntimeError(f"{self.settings.name} is lost!") + + if initial: + self.manager.interrupt_system(system) + yield self.env.process( + self.crew_transfer(system, subassembly, request, to_system=True) + ) + + current = self.env.simulation_time + + # If the hours required is longer than the shift time, then reset the available + # number of hours and the appropriate weather forecast, accounting for crew + # transfer, otherwise get an adequate weather window, allowing for interruptions + if not self.settings.non_stop_shift: + hours_available = hours_until_future_hour(current, end_shift) + hours_available -= self.settings.crew_transfer_time + *_, weather_forecast = self._weather_forecast( + hours_available, which="repair" + ) + else: + _, weather_forecast, _ = self.find_interrupted_weather_window( + hours_required + ) + + # Check that all times are within the windfarm's working hours and cut off any + # time points that fall outside of the work shifts + if hours_required > hours_available: + shift_delay = True + + hours_processed = 0 + weather_delay_groups = consecutive_groups(np.where(~weather_forecast)[0]) + while weather_forecast.shape[0] > 0 and hours_available > 0: + delays = np.where(~weather_forecast)[0] + if delays.size > 0: + hours_to_process = delays[0] + delay = weather_delay_groups.pop(0).size + else: + hours_to_process = weather_forecast.shape[0] + delay = 0 + weather_forecast = weather_forecast.slice(hours_to_process + delay) + + # If the delay is at the start, hours_to_process is 0, and a delay gets + # processed, otherwise the crew works for the minimum of + # ``hours_to_process`` or maximum time that can be worked until the shift's + # end, and maxed out by the hours required for the actual repair. + if hours_to_process > 0: + if hours_available <= hours_to_process: + hours_to_process = hours_available + else: + current = self.env.simulation_time + hours_to_process = hours_until_future_hour( + current, current.hour + int(hours_to_process) + ) + if hours_required < hours_to_process: + hours_to_process = hours_required + # Ensure this gets the correct float hours to the start of the target + # hour, unless the hours to process is between (0, 1] + shared_logging.update( + system_ol=system.operating_level, + part_ol=subassembly.operating_level, + ) + yield self.env.process( + self.process_repair( + hours_to_process, request.details, **shared_logging + ) + ) + hours_processed += hours_to_process + hours_available -= hours_to_process + hours_required -= hours_to_process + + # If a delay is the first part of the process or a delay occurs after the + # some work is performed, then that delay is processed here. + if delay > 0 and hours_required > 0: + current = self.env.simulation_time + hours_to_process = hours_until_future_hour( + current, current.hour + delay + ) + shared_logging.update( + system_ol=system.operating_level, + part_ol=subassembly.operating_level, + ) + yield self.env.process( + self.weather_delay( + hours_to_process, location="system", **shared_logging + ) + ) + hours_available -= hours_to_process + + # Reached the end of the shift or the end of the repair, and need to unload crew + # from the system + yield self.env.process( + self.crew_transfer(system, subassembly, request, to_system=False) + ) + if shift_delay or hours_required > 0: + # For 24-hour shifts, we just need to start back at the top + if self.settings.non_stop_shift: + yield self.env.process( + self.in_situ_repair( + request, + time_processed=hours_processed + time_processed, + prior_operation_level=starting_operational_level, + ) + ) + return + shared_logging.update( + system_ol=system.operating_level, part_ol=subassembly.operating_level + ) + yield self.env.process( + self.travel(start="site", end="port", **shared_logging) + ) + shared_logging.update( + system_ol=system.operating_level, part_ol=subassembly.operating_level + ) + yield self.env.process(self.wait_until_next_shift(**shared_logging)) + + yield self.env.process( + self.in_situ_repair( + request, + time_processed=hours_processed + time_processed, + prior_operation_level=starting_operational_level, + ) + ) + return + + # Register the repair + self.register_repair_with_subassembly( + subassembly, request, starting_operational_level + ) + action = "maintenance" if isinstance(request.details, Maintenance) else "repair" + self.env.log_action( + system_id=system.id, + system_name=system.name, + part_id=subassembly.id, + part_name=subassembly.name, + system_ol=system.operating_level, + part_ol=subassembly.operating_level, + agent=self.settings.name, + action=f"{action} complete", + reason=request.details.description, + materials_cost=request.details.materials, + additional="complete", + request_id=request.request_id, + location="system", + ) + + # If this is the end of the shift, ensure that we're traveling back to port + if not self.env.is_workshift(start_shift, end_shift): + shared_logging.update( + system_ol=system.operating_level, part_ol=subassembly.operating_level + ) + yield self.env.process( + self.travel(start="site", end="port", **shared_logging) + )
+ + +
+[docs] + def run_scheduled_in_situ(self) -> Generator[Process, None, None]: + """Runs the simulation of in situ repairs for scheduled servicing equipment + that have the `onsite` designation or don't require mobilization. + + Yields + ------ + Generator[Process, None, None] + The simulation. + """ + if TYPE_CHECKING: + assert isinstance(self.settings, ScheduledServiceEquipmentData) + + # If the starting operation date is the same as the simulations, set to onsite + if self.settings.operating_dates[0] == self.env.simulation_time.date(): + yield self.env.process(self.mobilize_scheduled()) + + while True: + # Wait for a valid operational period to start + if self.env.simulation_time.date() not in self.settings.operating_dates_set: + yield self.env.process(self.mobilize_scheduled()) + + # Wait for next shift to start + is_workshift = self.env.is_workshift( + workday_start=self.settings.workday_start, + workday_end=self.settings.workday_end, + ) + if not is_workshift: + yield self.env.process( + self.wait_until_next_shift( + **{ + "agent": self.settings.name, + "reason": "not in working hours", + "additional": "not in working hours", + } + ) + ) + + yield self.env.timeout(self.env.get_random_seconds(low=2)) + _, request = self.get_next_request() + if request is None: + if not self.at_port: + yield self.env.process( + self.travel( + start="site", + end="port", + reason="no requests", + agent=self.settings.name, + ) + ) + yield self.env.process( + self.wait_until_next_shift( + **{ + "agent": self.settings.name, + "reason": "no requests", + "additional": "no work requests, waiting until the next shift", # noqa: E501 + } + ) + ) + else: + yield self.manager.in_process_requests.put(request) + self.manager.request_status_map["pending"].difference_update( + [request.request_id] + ) + self.manager.request_status_map["processing"].update( + [request.request_id] + ) + + if request.cable: + system = self.windfarm.cable(request.system_id) + else: + system = self.windfarm.system(request.system_id) # type: ignore + yield system.servicing + yield self.env.process(self.in_situ_repair(request, initial=True))
+ + +
+[docs] + def run_unscheduled_in_situ(self) -> Generator[Process, None, None]: + """Runs an in situ repair simulation for unscheduled servicing equipment, or + those that have to be mobilized before performing repairs and maintenance. + + Yields + ------ + Generator[Process, None, None] + The simulation + """ + self.dispatched = True + + if TYPE_CHECKING: + assert isinstance(self.settings, UnscheduledServiceEquipmentData) + mobilization_days = self.settings.mobilization_days + charter_end_env_time = self.settings.charter_days * HOURS_IN_DAY + charter_end_env_time += mobilization_days * HOURS_IN_DAY + charter_end_env_time += self.env.now + + current = self.env.simulation_time + charter_start = current + pd.Timedelta(mobilization_days, "D") + charter_end = ( + current + + pd.Timedelta(mobilization_days, "D") + + pd.Timedelta(self.settings.charter_days, "D") + ) + charter_period = set(pd.date_range(charter_start, charter_end).date) + + # If the charter period contains any non-operational date, then, try again + # at the next available date after the end of the attempted charter period + intersection = charter_period.intersection( + self.settings.non_operational_dates_set + ) + if intersection: + intersection_end = max(intersection) + sim_end = self.env.end_datetime.date() + if intersection_end != sim_end: + hours_to_next = self.hours_to_next_operational_date( + start_search_date=intersection_end, + exclusion_days=mobilization_days, + ) + else: + hours_to_next = self.env.max_run_time - self.env.now + self.env.log_action( + agent=self.settings.name, + action="delay", + reason="non-operational period", + additional="waiting for next operational period", + duration=hours_to_next, + ) + yield self.env.timeout(hours_to_next) + yield self.env.process(self.run_unscheduled_in_situ()) + return + + while True and self.env.now < charter_end_env_time: + _, request = self.get_next_request() + if request is None: + yield self.env.process( + self.wait_until_next_shift( + **{ + "agent": self.settings.name, + "reason": "no requests", + "additional": "no work requests submitted by start of shift", # noqa: E501 + } + ) + ) + else: + break + + if self.env.now >= charter_end_env_time: + self.dispatched = False + return + + yield self.manager.in_process_requests.put(request) + self.manager.request_status_map["pending"].difference_update( + [request.request_id] + ) + self.manager.request_status_map["processing"].update([request.request_id]) + + # Ensure the system isn't in service already during the checking stages, then + # halt its ongoing processes for the current repair. + # NOTE: port-based equipment will have already halted the system's processes. + if not hasattr(self, "port"): + if request.cable: + system = self.windfarm.cable(request.system_id) + else: + system = self.windfarm.system(request.system_id) # type: ignore + yield system.servicing + yield self.env.timeout(self.env.get_random_seconds(low=2)) + yield system.servicing + + while True: + if self.env.now >= charter_end_env_time: + self.onsite = False + self.at_port = False + self.at_site = False + self.at_system = False + self.dispatched = False + self.current_system = None + self.env.log_action( + agent=self.settings.name, + action="leaving site", + reason="charter period has ended", + ) + break + + if not self.onsite: + # Mobilize and immediately run the repair logic for the inital request + yield self.env.process(self.mobilize()) + + if request.cable: + system = self.windfarm.cable(request.system_id) + else: + system = self.windfarm.system(request.system_id) # type: ignore + + yield system.servicing + yield self.env.process(self.in_situ_repair(request, initial=True)) + + # Wait for next shift to start + is_workshift = self.env.is_workshift( + workday_start=self.settings.workday_start, + workday_end=self.settings.workday_end, + ) + if not is_workshift: + yield self.env.process( + self.wait_until_next_shift( + **{ + "agent": self.settings.name, + "reason": "not in working hours", + "additional": "not in working hours", + } + ) + ) + + _, request = self.get_next_request() + if request is None: + yield self.env.process( + self.wait_until_next_shift( + **{ + "agent": self.settings.name, + "reason": "no requests", + "additional": "no work requests submitted by start of shift", # noqa: E501 + } + ) + ) + else: + if request.cable: + system = self.windfarm.cable(request.system_id) + else: + system = self.windfarm.system(request.system_id) # type: ignore + yield system.servicing + yield self.env.process(self.in_situ_repair(request, initial=True)) + self.dispatched = False
+ + +
+[docs] + def run_tow_to_port(self, request: RepairRequest) -> Generator[Process, None, None]: + """Runs the tow to port logic, so a turbine can be repaired at port. + + Parameters + ---------- + request : RepairRequest + The request the triggered the tow-to-port strategy. + + Yields + ------ + Generator[Process, None, None] + The series of events that simulate the complete towing logic. + + Raises + ------ + ValueError + Raised if the equipment is not currently at port + """ + self.dispatched = True + system = self.windfarm.system(request.system_id) + + shared_logging = { + "system_id": request.system_id, + "system_name": request.system_name, + "request_id": request.request_id, + "agent": self.settings.name, + } + + # Travel to the turbine + yield self.env.process( + self.travel( + "port", + "site", + set_current=system.id, + reason=request.details.description, + **shared_logging, # type: ignore + ) + ) + + # Turn off the turbine + self.manager.interrupt_system(system) + + # Unmoor the turbine and tow it back to port + yield self.env.process(self.mooring_connection(system, request, which="unmoor")) + yield self.env.process( + self.tow("site", "port", reason="towing turbine to port", **shared_logging) + ) + self.dispatched = False
+ + +
+[docs] + def run_tow_to_site( + self, request: RepairRequest, subassembly_resets: list[str] = [] + ) -> Generator[Process, None, None]: + """Runs the tow to site logic for after a turbine has had its repairs completed + at port. + + Parameters + ---------- + request : RepairRequest + The request the triggered the tow-to-port strategy. + subassembly_resets : list[str] + The `subassembly_id`s to reset to good as new. Defaults to []. + + Yields + ------ + Generator[Process, None, None] + The series of events that simulate the complete towing logic. + + Raises + ------ + ValueError + Raised if the equipment is not currently at port + """ + self.dispatched = True + system = self.windfarm.system(request.system_id) + shared_logging = { + "agent": self.settings.name, + "request_id": request.request_id, + "system_id": request.system_id, + "system_name": request.system_name, + } + + # Tow the turbine back to site and reconnect it to the mooring system + yield self.env.process( + self.tow( + "port", + "site", + set_current=system.id, + reason="towing turbine back to site", + **shared_logging, + ) + ) + yield self.env.process( + self.mooring_connection(system, request, which="reconnect") + ) + + # Reset the turbine back to operating and return to port + reset_system_operations(system, subassembly_resets) + self.manager.enable_requests_for_system(system, tow=True) + yield self.env.process( + self.travel( + "site", + "port", + reason="traveling back to port after returning turbine", + **shared_logging, # type: ignore + ) + ) + self.dispatched = False
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/wombat/core/simulation_api.html b/_modules/wombat/core/simulation_api.html new file mode 100644 index 00000000..e9a7b1c1 --- /dev/null +++ b/_modules/wombat/core/simulation_api.html @@ -0,0 +1,846 @@ + + + + + + + + + + wombat.core.simulation_api — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for wombat.core.simulation_api

+"""The main API for the ``wombat``."""
+from __future__ import annotations
+
+import datetime
+from typing import TYPE_CHECKING
+from pathlib import Path
+
+import yaml
+import numpy as np
+import pandas as pd
+from attrs import Attribute, field, define
+from simpy.events import Event
+
+from wombat.core import (
+    Metrics,
+    FromDictMixin,
+    RepairManager,
+    ServiceEquipment,
+    WombatEnvironment,
+)
+from wombat.windfarm import Windfarm
+from wombat.core.port import Port
+from wombat.core.library import load_yaml, library_map
+from wombat.core.data_classes import convert_to_list
+
+
+def _library_mapper(file_path: str | Path) -> Path:
+    """Attempts to extract a default library path if one of "DINWOODIE" or "IEA_26"
+    are passed, other returns ``file_path``.
+
+    Parameters
+    ----------
+    file_path : str | Path
+        Should be a valid file path, or one of "DINWOODIE" or "IEA_26" to indicate a
+        provided library is being used.
+
+    Returns
+    -------
+    str | Path
+        The library path.
+    """
+    return Path(library_map.get(file_path, file_path)).resolve()  # type: ignore
+
+
+
+[docs] +@define(frozen=True, auto_attribs=True) +class Configuration(FromDictMixin): + """The ``Simulation`` configuration data class that provides all the necessary + definitions. + + Parameters + ---------- + name: str + Name of the simulation. Used for logging files. + layout : str + The windfarm layout file. See ``wombat.Windfarm`` for more details. + service_equipment : str | list[str] + The equpiment that will be used in the simulation. See + ``wombat.core.ServiceEquipment`` for more details. + weather : str + The weather profile to be used. See ``wombat.simulation.WombatEnvironment`` + for more details. + workday_start : int + Starting hour for a typical work shift. Can be overridden by + equipment-specific settings. + workday_end : int + Ending hour for a typical work shift. Can be overridden by + equipment-specific settings. + inflation_rate : float + The annual inflation rate to be used for post-processing. + fixed_costs : str + The file name for the fixed costs assumptions. + project_capacity : int | float + The total capacity of the wind plant, in MW. + port : dict | str | Path + The port configuration file or dictionary that will be used to setup a + tow-to-port repair strategy, default None. + port_distance : int | float + The simulation-wide daily travel distance for servicing equipment. This should + be used as a base setting when multiple or all servicing equipment will be + operating out of the same base location, but can be individually modified. + start_year : int + Start year of the simulation. The exact date will be determined by + the first valid date of this year in ``weather``. + end_year : int + Final year of the simulation. The exact date will be determined by + the last valid date of this year in ``weather``. + non_operational_start : str | datetime.datetime | None + The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of prohibited operations. When defined at the environment level, an + undefined or later starting date will be overridden for all servicing equipment + and any modeled port, by default None. + non_operational_end : str | datetime.datetime | None + The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of prohibited operations. When defined at the environment level, an + undefined or earlier ending date will be overridden for all servicing equipment + and any modeled port, by default None. + reduced_speed_start : str | datetime.datetime | None + The starting month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of reduced speed operations. When defined at the environment level, an + undefined or later starting date will be overridden for all servicing equipment + and any modeled port, by default None. + reduced_speed_end : str | datetime.datetime | None + The ending month and day, e.g., MM/DD, M/D, MM-DD, etc. for an annualized + period of reduced speed operations. When defined at the environment level, an + undefined or earlier ending date will be overridden for all servicing equipment + and any modeled port, by default None. + reduced_speed : float + The maximum operating speed during the annualized reduced speed operations. + When defined at the environment level, an undefined or faster value will be + overridden for all servicing equipment and any modeled port, by default 0.0. + random_seed : int | None + The random seed to be passed to a universal NumPy ``default_rng`` object to + generate Weibull random generators, by default None. + random_generator: np.random._generator.Generator | None + An optional numpy random generator that can be provided to seed a simulation + with the same generator each time, in place of the random seed. If a + :py:attr:`random_seed` is also provided, this will override the random seed, + by default None. + """ + + name: str + layout: str + service_equipment: str | list[str] = field(converter=convert_to_list) + weather: str | pd.DataFrame + workday_start: int = field(converter=int) + workday_end: int = field(converter=int) + inflation_rate: float = field(converter=float) + project_capacity: int | float = field(converter=float) + fixed_costs: dict | str | Path = field(default=None) + port: dict | str | Path = field(default=None) + start_year: int = field(default=None) + end_year: int = field(default=None) + port_distance: int | float = field(default=None) + non_operational_start: str | datetime.datetime | None = field(default=None) + non_operational_end: str | datetime.datetime | None = field(default=None) + reduced_speed_start: str | datetime.datetime | None = field(default=None) + reduced_speed_end: str | datetime.datetime | None = field(default=None) + reduced_speed: float = field(default=0.0) + random_seed: int | None = field(default=None) + random_generator: np.random._generator.Generator | None = field(default=None)
+ + + +
+[docs] +@define(auto_attribs=True) +class Simulation(FromDictMixin): + """The primary API to interact with the simulation methodologies. + + Parameters + ---------- + library_path : str + The path to the main data library. + config : Configuration | dict | str + One of the following: + - A pre-loaded ``Configuration`` object + - A dictionary ready to be converted to a ``Configuration`` object + - The name of the configuration file to be loaded, that will be located at: + ``library_path`` / config / ``config`` + random_seed : int | None + The random seed to be passed to a universal NumPy ``default_rng`` object to + generate Weibull random generators, by default None. + random_generator: np.random._generator.Generator | None + An optional numpy random generator that can be provided to seed a simulation + with the same generator each time, in place of the random seed. If a + :py:attr:`random_seed` is also provided, this will override the random seed, + by default None. + """ + + library_path: Path = field(converter=_library_mapper) + config: Configuration = field() + random_seed: int | None = field(default=None) + random_generator: np.random._generator.Generator | None = field(default=None) + + metrics: Metrics = field(init=False) + windfarm: Windfarm = field(init=False) + env: WombatEnvironment = field(init=False) + repair_manager: RepairManager = field(init=False) + service_equipment: list[ServiceEquipment] = field(init=False) + port: Port = field(init=False) + + def __attrs_post_init__(self) -> None: + """Post-initialization hook.""" + self._setup_simulation() + +
+[docs] + @config.validator # type: ignore + def _create_configuration( + self, attribute: Attribute, value: str | Path | dict | Configuration + ) -> None: + """Validates the configuration object and creates the ``Configuration`` object + for the simulation. + + Raises + ------ + TypeError + Raised if the value provided is not able to create a valid ``Configuration`` + object + ValueError + Raised if ``name`` and ``config.name`` or ``library_path`` and + ``config.library`` are not aligned. + + Returns + ------- + Configuration + The validated simulation configuration + """ + if isinstance(value, (str, Path)): + value = load_yaml(self.library_path / "project/config", value) + if isinstance(value, dict): + value = Configuration.from_dict(value) + if isinstance(value, Configuration): + object.__setattr__(self, attribute.name, value) + else: + raise TypeError( + "``config`` must be a dictionary, valid file path to a yaml-enocoded", + "dictionary, or ``Configuration`` object!", + )
+ + +
+[docs] + @classmethod + def from_config( + cls, library_path: str | Path, config: str | Path | dict | Configuration + ): + """Creates the ``Simulation`` object only the configuration contents as either a + full file path to the configuration file, a dictionary of the configuration + contents, or pre-loaded ``Configuration`` object. + + Parameters + ---------- + library_path : str | Path + The simulation's data library. If a filename is provided for + :py:attr:`config`, this is the data library from where it will be imported. + This will also be used to feed into the returned `Simulation.library_path`. + config : str | Path | dict | Configuration + The simulation configuration, see ``Configuration`` for more details on the + contents. The following is a description of the acceptable contents: + + - ``str`` : the full file path of the configuration yaml file. + - ``dict`` : a dictionary with the requried configuration settings. + - ``Configuration`` : a pre-created ``Configuration`` object. + + Raises + ------ + TypeError + Raised if ``config`` is not one of the three acceptable input types. + + Returns + ------- + Simulation + A ready-to-run ``Simulation`` object. + """ + library_path = _library_mapper(library_path) + if isinstance(config, (str, Path)): + config = library_path / "project" / "config" / config + config = load_yaml(config.parent, config.name) + if isinstance(config, dict): + config = Configuration.from_dict(config) + if not isinstance(config, Configuration): + raise TypeError( + "``config`` must be a dictionary or ``Configuration`` object!" + ) + if TYPE_CHECKING: + assert isinstance(config, Configuration) # mypy helper + return cls( # type: ignore + library_path=library_path, + config=config, + random_seed=config.random_seed, + random_generator=config.random_generator, + )
+ + +
+[docs] + def _setup_simulation(self): + """Initializes the simulation objects.""" + self.env = WombatEnvironment( + self.library_path, + self.config.weather, + simulation_name=self.config.name, + workday_start=self.config.workday_start, + workday_end=self.config.workday_end, + start_year=self.config.start_year, + end_year=self.config.end_year, + port_distance=self.config.port_distance, + non_operational_start=self.config.non_operational_start, + non_operational_end=self.config.non_operational_end, + reduced_speed_start=self.config.reduced_speed_start, + reduced_speed_end=self.config.reduced_speed_end, + reduced_speed=self.config.reduced_speed, + random_seed=self.random_seed, + random_generator=self.random_generator, + ) + self.repair_manager = RepairManager(self.env) + self.windfarm = Windfarm(self.env, self.config.layout, self.repair_manager) + + # Create the servicing equipment and set the necessary environment variables + self.service_equipment: dict[str, ServiceEquipment] = {} # type: ignore + for service_equipment in self.config.service_equipment: + equipment = ServiceEquipment( + self.env, self.windfarm, self.repair_manager, service_equipment + ) + equipment.finish_setup_with_environment_variables() + name = equipment.settings.name + if name in self.service_equipment: + raise ValueError( + f"Servicing equipment `{name}` already exists, please use unique" + " names for all servicing equipment." + ) + self.service_equipment[name] = equipment # type: ignore + + # Create the port and add any tugboats to the available servicing equipment list + if self.config.port is not None: + self.port = Port( + self.env, self.windfarm, self.repair_manager, self.config.port + ) + for service_equipment in self.port.service_equipment_manager.items: + name = service_equipment.settings.name # type: ignore + if name in self.service_equipment: + raise ValueError( + f"Servicing equipment `{name}` already exists, please use" + " unique names for all servicing equipment." + ) + self.service_equipment[name] = service_equipment # type: ignore + + if self.config.project_capacity * 1000 != round(self.windfarm.capacity, 6): + raise ValueError( + f"Input `project_capacity`: {self.config.project_capacity:,.6f} MW is" + f" not equal to the sum of turbine capacities:" + f" {self.windfarm.capacity / 1000:,.6f} MW" + )
+ + +
+[docs] + def run( + self, + until: int | float | Event | None = None, + create_metrics: bool = True, + save_metrics_inputs: bool = True, + ): + """Calls ``WombatEnvironment.run()`` and gathers the results for + post-processing. See ``wombat.simulation.WombatEnvironment.run`` or + ``simpy.Environment.run`` for more details. + + Parameters + ---------- + until : Optional[int | float | Event], optional + When to stop the simulation, by default None. See documentation on + ``simpy.Environment.run`` for more details. + create_metrics : bool, optional + If True, the metrics object will be created, and not, if False, by default + True. + save_metrics_inputs : bool, optional + If True, the metrics inputs data will be saved to a yaml file, with file + references to any larger data structures that can be reloaded later. If + False, the data will not be saved, by default True. + """ + self.env.run(until=until) + if save_metrics_inputs: + self.save_metrics_inputs() + if create_metrics: + self.initialize_metrics()
+ + + def initialize_metrics(self) -> None: + """Instantiates the ``metrics`` attribute after the simulation is run.""" + events = self.env.load_events_log_dataframe() + operations = self.env.load_operations_log_dataframe() + power_potential, power_production = self.env.power_production_potential_to_csv( + windfarm=self.windfarm, operations=operations, return_df=True + ) + substation_turbine_map = { + s_id: {k: v.tolist() for k, v in dict.items()} + for s_id, dict in self.windfarm.substation_turbine_map.items() + } + capacities = [ + self.windfarm.system(t).capacity for t in self.windfarm.turbine_id + ] + self.metrics = Metrics( + data_dir=self.library_path, + events=events, + operations=operations, + potential=power_potential, + production=power_production, + inflation_rate=self.config.inflation_rate, + project_capacity=self.config.project_capacity, + turbine_capacities=capacities, + fixed_costs=self.config.fixed_costs, # type: ignore + substation_id=self.windfarm.substation_id.tolist(), + turbine_id=self.windfarm.turbine_id.tolist(), + substation_turbine_map=substation_turbine_map, + service_equipment_names=[*self.service_equipment], # type: ignore + ) + +
+[docs] + def save_metrics_inputs(self) -> None: + """Saves the inputs for the `Metrics` initialization with either a direct + copy of the data or a file reference that can be loaded later. + """ + substation_turbine_map = { + s_id: {k: v.tolist() for k, v in dict.items()} + for s_id, dict in self.windfarm.substation_turbine_map.items() + } + data = { + "data_dir": str(self.library_path), + "events": str(self.env.events_log_fname), + "operations": str(self.env.operations_log_fname), + "potential": str(self.env.power_potential_fname), + "production": str(self.env.power_production_fname), + "inflation_rate": self.config.inflation_rate, + "project_capacity": self.config.project_capacity, + "turbine_capacities": [ + self.windfarm.system(t_id).capacity for t_id in self.windfarm.turbine_id + ], + "fixed_costs": self.config.fixed_costs, + "substation_id": self.windfarm.substation_id.tolist(), + "turbine_id": self.windfarm.turbine_id.tolist(), + "substation_turbine_map": substation_turbine_map, + "service_equipment_names": [*self.service_equipment], + } + + with open(self.env.metrics_input_fname, "w") as f: + yaml.dump(data, f, default_flow_style=False, sort_keys=False)
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/wombat/utilities/logging.html b/_modules/wombat/utilities/logging.html new file mode 100644 index 00000000..48f84ffc --- /dev/null +++ b/_modules/wombat/utilities/logging.html @@ -0,0 +1,541 @@ + + + + + + + + + + wombat.utilities.logging — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for wombat.utilities.logging

+"""General logging methods."""
+
+from __future__ import annotations
+
+import logging
+import datetime
+from typing import Any
+from pathlib import Path
+from logging.handlers import MemoryHandler
+
+
+
+[docs] +def setup_logger( + logger_name: str, log_file: Path, level: Any = logging.INFO, capacity: int = 500 +) -> None: + """Creates the logging infrastructure for a given logging category. + + TODO: Figure out how to type check ``logging.INFO``; ``Callable``? + + Parameters + ---------- + logger_name : str + Name to assign to the logger. + log_file : Path + File name and path for where the log data should be saved. + level : Any, optional + Logging level, by default logging.INFO. + """ + logger = logging.getLogger(logger_name) + formatter = logging.Formatter("%(asctime)s|%(name)s|%(levelname)s|%(message)s") + fileHandler = logging.FileHandler(log_file, mode="w") + fileHandler.setFormatter(formatter) + + memory_handler = MemoryHandler(capacity=capacity, target=fileHandler) + memory_handler.setFormatter(formatter) + + logger.setLevel(level) + # logger.addHandler(fileHandler) + logger.addHandler(memory_handler)
+ + + +
+[docs] +def format_events_log_message( + simulation_time: datetime.datetime, + env_time: float, + system_id: str, + system_name: str, + part_id: str, + part_name: str, + system_ol: float | str, + part_ol: float | str, + agent: str, + action: str, + reason: str, + additional: str, + duration: float, + request_id: str, + location: str = "na", + materials_cost: int | float = 0, + hourly_labor_cost: int | float = 0, + salary_labor_cost: int | float = 0, + equipment_cost: int | float = 0, +) -> str: + """Formats the logging messages into the expected format for logging. + + Parameters + ---------- + simulation_time : datetime64 + Timestamp within the simulation time. + env_time : float + Environment simulation time (``Environment.now``). + system_id : str + Turbine ID, ``System.id``. + system_name : str + Turbine name, ``System.name``. + part_id : str + Subassembly, component, or cable ID, ``_.id``. + part_name : str + Subassembly, component, or cable name, ``_.name``. + system_ol : int | float + System operating level, ``System.operating_level``. Use an empty string for n/a. + part_ol : int | float + Subassembly, component, or cable operating level, ``_.operating_level``. Use an + empty string for n/a. + agent : str + Agent performin the action. + action : str + Action that was taken. + reason : str + Reason an action was taken. + additional : str + Any additional information that needs to be logged. + duration : float + Length of time the action lasted. + request_id : str + The ``RepairRequest.request_id`` or "na". + location : str + The location of where the event ocurred: should be one of site, port, + enroute, or system, by default "na". + materials_cost : int | float, optional + Total cost of materials for action, in USD, by default 0. + hourly_labor_cost : int | float, optional + Total cost of hourly labor for action, in USD, by default 0. + salary_labor_cost : int | float, optional + Total cost of salaried labor for action, in USD, by default 0. + equipment_cost : int | float, optional + Total cost of equipment for action, in USD, by default 0. + + Returns + ------- + str + Formatted message for consistent logging.[summary] + """ + total_labor_cost = hourly_labor_cost + salary_labor_cost + total_cost = total_labor_cost + equipment_cost + materials_cost + message = ( + f"{simulation_time}|{env_time:f}|{system_id}|{system_name}|{part_id}" + f"|{part_name}|{system_ol:f}|{part_ol:f}|{agent}|{action}" + f"|{reason}|{additional}|{duration:f}|{request_id}|{location}" + f"|{materials_cost:f}|{hourly_labor_cost:f}|{salary_labor_cost:f}" + f"|{equipment_cost:f}|{total_labor_cost:f}|{total_cost:f}" + ) + return message
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/wombat/utilities/plot.html b/_modules/wombat/utilities/plot.html new file mode 100644 index 00000000..a1e27657 --- /dev/null +++ b/_modules/wombat/utilities/plot.html @@ -0,0 +1,767 @@ + + + + + + + + + + wombat.utilities.plot — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for wombat.utilities.plot

+"""Provides expoerimental plotting routines to help with simulation diagnostics."""
+
+import numpy as np
+import pandas as pd
+import networkx as nx
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+
+from wombat import Simulation
+from wombat.windfarm import Windfarm
+
+
+
+[docs] +def plot_farm_layout( + windfarm: Windfarm, + figure_kwargs: dict | None = None, + plot_kwargs: dict | None = None, + return_fig: bool = False, +) -> None | tuple[plt.figure, plt.axes]: + """Plot the graph representation of the windfarm as represented through WOMBAT. + + Args: + figure_kwargs : dict, optional + Customized keyword arguments for matplotlib figure instantiation that + will passed as ``plt.figure(**figure_kwargs). Defaults to {}.`` + plot_kwargs : dict, optional + Customized parameters for ``networkx.draw()`` that can will passed as + ``nx.draw(**figure_kwargs)``. Defaults to ``with_labels=True``, + ``horizontalalignment=right``, ``verticalalignment=bottom``, + ``font_weight=bold``, ``font_size=10``, and ``node_color=#E37225``. + return_fig : bool, optional + Whether or not to return the figure and axes objects for further editing + and/or saving. Defaults to False. + + Returns + ------- + None | tuple[plt.figure, plt.axes]: _description_ + """ + # Set the defaults for plotting + if figure_kwargs is None: + figure_kwargs = {} + if plot_kwargs is None: + plot_kwargs = {} + figure_kwargs.setdefault("figsize", (14, 12)) + figure_kwargs.setdefault("dpi", 200) + plot_kwargs.setdefault("with_labels", True) + plot_kwargs.setdefault("horizontalalignment", "right") + plot_kwargs.setdefault("verticalalignment", "bottom") + plot_kwargs.setdefault("font_weight", "bold") + plot_kwargs.setdefault("font_size", 10) + plot_kwargs.setdefault("node_color", "#E37225") + + fig = plt.figure(**figure_kwargs) + ax = fig.add_subplot(111) + + # Get the node positions and all related edges, except the self-connected ones + positions = { + name: np.array([node["longitude"], node["latitude"]]) + for name, node in windfarm.graph.nodes(data=True) + } + edges = [el for el in windfarm.graph.edges if el[0] != el[1]] + + nx.draw(windfarm.graph, pos=positions, edgelist=edges, ax=ax, **plot_kwargs) + + fig.tight_layout() + plt.show() + + if return_fig: + return fig, ax + return None
+ + + +
+[docs] +def plot_farm_availability( + sim: Simulation, + which: str = "energy", + individual_turbines: bool = False, + farm_95_CI: bool = False, + figure_kwargs: dict | None = None, + plot_kwargs: dict | None = None, + legend_kwargs: dict | None = None, + tick_fontsize: int = 12, + label_fontsize: int = 16, + return_fig: bool = False, +) -> tuple[plt.Figure | plt.Axes] | None: + """Plots a line chart of the monthly availability at the wind farm level. + + Parameters + ---------- + sim : Simulation + A ``Simulation`` object that has been run. + which : str + One of "time" or "energy", to indicate the basis for the availability + calculation, by default "energy". + individual_turbines : bool, optional + Indicates if faint gray lines should be added in the background for the + availability of each turbine, by default False. + farm_95_CI : bool, optional + Indicates if the 95% CI area fill should be added in the background. + figure_kwargs : dict, optional + Custom parameters for ``plt.figure()``, by default ``figsize=(15, 7)`` and + ``dpi=300``. + plot_kwargs : dict, optional + Custom parameters to be passed to ``ax.plot()``, by default a label consisting + of the simulation name and project-level availability. + legend_kwargs : dict, optional + Custom parameters to be passed to ``ax.legend()``, by default ``fontsize=14``. + tick_fontsize : int, optional + The x- and y-axis tick label fontsize, by default 12. + label_fontsize : int, optional + The x- and y-axis label fontsize, by default 16. + return_fig : bool, optional + If ``True``, return the figure and Axes object, otherwise don't, by default + False. + + Returns + ------- + tuple[plt.Figure, plt.Axes] | None + See :py:attr:`return_fig` for details. + _description_ + """ + # Get the availability data + metrics = sim.metrics + if which == "time": + availability = metrics.time_based_availability("project", "windfarm").values[0][ + 0 + ] + windfarm_availability = metrics.time_based_availability( + "month-year", "windfarm" + ) + turbine_availability = metrics.time_based_availability("month-year", "turbine") + label = f"{sim.env.simulation_name} Time-Based Availability: {availability:.2%}" + elif which == "energy": + availability = metrics.production_based_availability( + "project", "windfarm" + ).values[0][0] + windfarm_availability = metrics.production_based_availability( + "month-year", "windfarm" + ) + turbine_availability = metrics.production_based_availability( + "month-year", "turbine" + ) + label = ( + f"{sim.env.simulation_name} Energy-Based Availability: {availability:.2%}" + ) + else: + raise ValueError("`which` must be one of 'energy' or 'time'.") + + # Set the defaults + if figure_kwargs is None: + figure_kwargs = {} + if plot_kwargs is None: + plot_kwargs = {} + if legend_kwargs is None: + legend_kwargs = {} + figure_kwargs.setdefault("figsize", (15, 7)) + figure_kwargs.setdefault("dpi", 300) + plot_kwargs.setdefault("label", label) + legend_kwargs.setdefault("fontsize", 14) + + fig = plt.figure(**figure_kwargs) + ax = fig.add_subplot(111) + + x = range(windfarm_availability.shape[0]) + + # Individual turbine availability lines + if individual_turbines: + for turbine in turbine_availability.columns: + ax.plot( + x, + turbine_availability[turbine].values, + color="lightgray", + alpha=0.75, + linewidth=0.25, + ) + + # CI error bars surrounding the lines + if farm_95_CI: + N = turbine_availability.shape[1] + Z90, Z95, Z99 = (1.64, 1.96, 2.57) # noqa: F841 + tm = turbine_availability.values.mean(axis=1) + tsd = turbine_availability.values.std(axis=1) + ci_lo = tm - Z95 * (tsd / np.sqrt(N)) + ci_hi = tm + Z95 * (tsd / np.sqrt(N)) + ax.fill_between( + x, ci_lo, ci_hi, alpha=0.5, label="95% CI for Individual Turbines" + ) + + ax.plot( + x, + windfarm_availability.windfarm.values, + **plot_kwargs, + ) + + years = list(range(sim.env.start_year, sim.env.end_year + 1)) + xticks_major = [x * 12 for x in range(len(years))] + xticks_minor = list(range(0, 12 * len(years), 3)) + xlabels_major = [f"{year:>6}" for year in years] + xlabels_minor = ["", "Apr", "", "Oct"] * len(years) + + ax.set_ylim(0, 1) + ax.set_yticks(np.linspace(0, 1, 11)) + ax.set_yticklabels([f"{y:.0%}" for y in ax.get_yticks()], fontsize=tick_fontsize) + + ax.set_xlim(0, windfarm_availability.shape[0]) + ax.set_xticks(xticks_major) + for t in ax.get_xticklabels(): + t.set_y(-0.05) + ax.set_xticks(xticks_minor, minor=True) + ax.set_xticklabels(xlabels_major, ha="left", fontsize=tick_fontsize) + ax.set_xticklabels(xlabels_minor, minor=True, rotation=90) + + ax.set_xlabel("Simulation Time", fontsize=label_fontsize) + ax.set_ylabel("Monthly Availability", fontsize=label_fontsize) + + ax.legend(**legend_kwargs) + + ax.grid(axis="both", which="major") + ax.grid(alpha=0.5, which="minor") + + fig.tight_layout() + if return_fig: + return fig, ax # type: ignore + return None
+ + + +
+[docs] +def plot_operational_levels( + sim: Simulation, + figure_kwargs: dict | None = None, + cbar_label_fontsize: int = 14, + return_fig: bool = False, +): + """Plots an hourly view of the operational levels of the wind farm and individual + turbines as a heatmap. + + Parameters + ---------- + sim : Simulation + A ``Simulation`` object that has been run. + figure_kwargs : dict, optional + Custom settings for ``plt.figure()``, by default ``figsize=(15, 10)`` + and ``dpi=300``. + cbar_label_fontsize : int, optional + The default fontsize used in the color bar legend for the axis label, by default + 14. + return_fig : bool, optional + If ``True``, return the figure and Axes object, otherwise don't, by default + False. + + Returns + ------- + tuple[plt.Figure, plt.Axes] | None + See :py:attr:`return_fig` for details. + """ + # Set the defaults + if figure_kwargs is None: + figure_kwargs = {} + figure_kwargs.setdefault("figsize", (15, 10)) + figure_kwargs.setdefault("dpi", 300) + + # Get the requisite data + op = sim.metrics.operations + x_ix = [ + c + for c in op.columns + if c not in ("env_datetime", "env_time", "year", "month", "day") + ] + turbines = op[x_ix].values.astype(float).T + env_time = op.env_time.values + env_datetime = op.env_datetime.reset_index(drop=True) + + fig = plt.figure(**figure_kwargs) + ax = fig.add_subplot(111) + + # Create the custom color map + bounds = [0, 0.05, 0.5, 0.75, 0.9, 0.95, 1] + YlGnBu_discrete7 = mpl.colors.ListedColormap( + ["#ffffcc", "#c7e9b4", "#7fcdbb", "#41b6c4", "#1d91c0", "#225ea8", "#0c2c84"] + ) + norm = mpl.colors.BoundaryNorm(bounds, YlGnBu_discrete7.N) + + # Plot the turbine availability + ax.imshow( + turbines, aspect="auto", cmap=YlGnBu_discrete7, norm=norm, interpolation="none" + ) + + # Format the y-axis + ax.set_yticks(np.arange(len(x_ix))) + ax.set_yticklabels(x_ix) + ax.set_ylim(-0.5, len(x_ix) - 0.5) + ax.hlines( + np.arange(len(x_ix)) + 0.5, + env_time.min(), + env_time.max(), + color="white", + linewidth=1, + ) + + # Create the major x-tick data + env_dt_yr = env_datetime.dt.year + ix = ~env_dt_yr.duplicated() + x_major_ticks = env_dt_yr.loc[ix].index[1:] + x_major_ticklabels = env_dt_yr.loc[ix].values[1:] + + # Create the minor x-tick data + env_datetime_df = env_datetime.to_frame() + env_datetime_df["month"] = env_datetime_df.env_datetime.dt.month + env_datetime_df["month_year"] = pd.DatetimeIndex( + env_datetime_df.env_datetime + ).to_period("m") + ix = ~env_datetime_df.month_year.duplicated() & ( + env_datetime_df.month.isin((4, 7, 10)) + ) + x_minor_ticks = env_datetime_df.loc[ix].index.values + x_minor_ticklabels = [ + "" if x == "Jul" else x + for x in env_datetime_df.loc[ix].env_datetime.dt.strftime("%b") + ] + + # Set the x-ticks + ax.set_xticks(x_major_ticks) + ax.set_xticklabels(x_major_ticklabels) + for t in ax.get_xticklabels(): + t.set_y(-0.05) + ax.set_xticks(x_minor_ticks, minor=True) + ax.set_xticklabels(x_minor_ticklabels, minor=True, rotation=90) + + # Create a color bar legend that is propportionally spaced + cbar = ax.figure.colorbar( + mpl.cm.ScalarMappable(cmap=YlGnBu_discrete7, norm=norm), + ax=ax, + ticks=bounds, + spacing="proportional", + format=mpl.ticker.PercentFormatter(xmax=1), + ) + cbar.ax.set_ylabel( + "Availability", rotation=-90, va="bottom", fontsize=cbar_label_fontsize + ) + + ax.grid(axis="x", which="major") + ax.grid(alpha=0.7, axis="x", which="minor") + + fig.tight_layout() + if return_fig: + return fig, ax + return None
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/wombat/utilities/time.html b/_modules/wombat/utilities/time.html new file mode 100644 index 00000000..eefbf4f3 --- /dev/null +++ b/_modules/wombat/utilities/time.html @@ -0,0 +1,575 @@ + + + + + + + + + + wombat.utilities.time — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for wombat.utilities.time

+"""General methods for time-based calculations."""
+
+from __future__ import annotations
+
+import datetime
+
+from dateutil.parser import parse
+
+
+HOURS_IN_DAY = 24
+HOURS_IN_YEAR = 8760
+
+
+
+[docs] +def parse_date(value: str | None | datetime.datetime) -> datetime.datetime | None: + """Thin wrapper for ``dateutil.parser.parse`` that converts string dates and returns + back None or the original value if it's None or a ``datetime.datetime`` object, + respectively. + + Parameters + ---------- + value : str | None | datetime.datetime + A month/date or month-date formatted string to be converted to a + ``datetime.datetime`` object, or ``datetime.datetime`` object, or None. + + Returns + ------- + datetime.datetime | None + A converted ``datetime.datetime`` object or None. + """ + if value is None: + return value + if isinstance(value, datetime.datetime): + return value + + # Ensure there is a common comparison year for all datetime values + return parse(value).replace(year=2022)
+ + + +
+[docs] +def convert_dt_to_hours(diff: datetime.timedelta) -> float: + """Convert a ``datetime.timedelta`` object to number of hours at the seconds + resolution. + + Parameters + ---------- + diff : datetime.timedelta + The difference between two ``datetime.datetime`` objects. + + Returns + ------- + float + Number of hours between to ``datetime.datetime`` objects. + """ + days = diff.days * 24 if diff.days > 0 else 0 + seconds = diff.seconds / 60 / 60 + return days + seconds
+ + + +
+[docs] +def hours_until_future_hour(dt: datetime.datetime, hour: int) -> float: + """Number of hours until a future hour in the same day for ``hour`` <= 24, + otherwise, it is the number of hours until a time in the proceeding days. + + Parameters + ---------- + dt : datetime.datetime + Focal datetime. + hour : int + Hour that is later in the day, in 24 hour time. + + Returns + ------- + float + Number of hours between the two times. + """ + if hour >= 24: + days, hour = divmod(hour, 24) + + # Convert to python int + days = int(days) + hour = int(hour) + + new_dt = dt + datetime.timedelta(days=days) + new_dt = new_dt.replace(hour=hour, minute=0, second=0) + else: + new_dt = dt.replace(hour=hour, minute=0, second=0) + diff = new_dt - dt + return convert_dt_to_hours(diff)
+ + + +
+[docs] +def check_working_hours( + env_start: int, env_end: int, workday_start: int, workday_end: int +) -> tuple[int, int]: + """Checks the working hours of a port or servicing equipment, and overrides a + default (-1) to the environment's settings, otherwise returns back the input hours. + + Parameters + ---------- + env_start : int + The starting hour for the environment's shift + env_end : int + The ending hour for the environment's shift + workday_start : int + The starting hour to be checked. + workday_end : int + The ending hour to be checked. + + Returns + ------- + tuple[int, int] + The starting and ending hour to be applied back to the port or servicing + equipment. + """ + start_is_invalid = workday_start == -1 + end_is_invalid = workday_end == -1 + + start = env_start if start_is_invalid else workday_start + end = env_end if end_is_invalid else workday_end + + return start, end
+ + + +
+[docs] +def calculate_cost( + duration: int | float, rate: float, n_rate: int = 1, daily_rate: bool = False +) -> float: + """Calculates the equipment cost, or labor cost for either salaried or hourly + employees. + + Parameters + ---------- + duration : int | float + Length of time, in hours. + rate : float + The labor or equipment rate, in $USD/hour. + n_rate : int + Total number of of the rate to be applied, more than one if``rate`` is broken + down by number of individual laborers (if rate is a labor rate), by default 1. + daily_rate : bool, optional + Indicator for if the ``rate`` is a daily rate (True), or hourly rate (False), by + default False. + + Returns + ------- + float + The total cost of the labor performed. + """ + multiplier = duration / HOURS_IN_DAY if daily_rate else duration + return multiplier * rate * n_rate
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/wombat/utilities/utilities.html b/_modules/wombat/utilities/utilities.html new file mode 100644 index 00000000..5f6234a9 --- /dev/null +++ b/_modules/wombat/utilities/utilities.html @@ -0,0 +1,555 @@ + + + + + + + + + + wombat.utilities.utilities — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for wombat.utilities.utilities

+"""Provides various utility functions that don't fit within a common theme."""
+
+
+from __future__ import annotations
+
+import re
+from typing import Callable
+
+import numpy as np
+import pandas as pd
+
+
+try:
+    from functools import cache  # type: ignore
+except ImportError:
+    from functools import lru_cache
+
+    cache = lru_cache(None)
+
+
+# Don't repeat the most common inputs that occur when there are no state changes, but
+# the result is needed again for logging
+
+[docs] +@cache +def _mean(*args) -> float: + """Multiplies two numbers. Used for a reduce operation. + + Parameters + ---------- + args : int | float + The values to compute the mean over + + Returns + ------- + float + The average of the values provided + """ + return np.mean(args)
+ + + +
+[docs] +def create_variable_from_string(string: str) -> str: + """Creates a valid Python variable style string from a passed string. + + Parameters + ---------- + string : str + The string to convert into a Python-friendly variable name. + + Returns + ------- + str + A Python-valid variable name. + + Examples + -------- + >>> example_string = "*Electrical!*_ _System$*_" + >>> print(create_variable_from_string(example_string)) + 'electrical_system' + + """ + new_string = re.sub( + "[^0-9a-zA-Z]+", + "_", + re.sub(r"^\W+", "", re.sub("[^a-zA-Z]+$", "", string)), # noqa: disable=W605 + ).lower() + return new_string
+ + + +
+[docs] +def IEC_power_curve( + windspeed_column: np.ndarray | pd.Series, + power_column: np.ndarray | pd.Series, + bin_width: float = 0.5, + windspeed_start: float = 0.0, + windspeed_end: float = 30.0, +) -> Callable: + """Direct copyfrom OpenOA: + https://github.com/NREL/OpenOA/blob/main/operational_analysis/toolkits/power_curve/functions.py#L16-L57 + Use IEC 61400-12-1-2 method for creating wind-speed binned power curve. + + Parameters + ---------- + windspeed_column : np.ndarray | pandas.Series + The power curve's windspeed values, in m/s. + power_column : np.ndarray | pandas.Series + The power curve's output power values, in kW. + bin_width : float + Width of windspeed bin, default is 0.5 m/s according to standard, by default + 0.5. + windspeed_start : float + Left edge of first windspeed bin, where all proceeding values will be 0.0, + by default 0.0. + windspeed_end : float + Right edge of last windspeed bin, where all following values will be 0.0, by + default 30.0. + + Returns + ------- + Callable + Python function of the power curve, of type (Array[float] -> Array[float]), + that maps input windspeed value(s) to ouptut power value(s). + """ + if not isinstance(windspeed_column, pd.Series): + windspeed_column = pd.Series(windspeed_column) + if not isinstance(power_column, pd.Series): + power_column = pd.Series(power_column) + + # Set up evenly spaced bins of fixed width, with np.inf for any value over the max + n_bins = int(np.ceil((windspeed_end - windspeed_start) / bin_width)) + 1 + bins = np.append(np.linspace(windspeed_start, windspeed_end, n_bins), [np.inf]) + + # Initialize an array which will hold the mean values of each bin + P_bin = np.ones(len(bins) - 1) * np.nan + + # Compute the mean of each bin and set corresponding P_bin + for ibin in range(0, len(bins) - 1): + indices = (windspeed_column >= bins[ibin]) & (windspeed_column < bins[ibin + 1]) + P_bin[ibin] = power_column.loc[indices].mean() + + # Linearly interpolate any missing bins + P_bin = pd.Series(data=P_bin).interpolate(method="linear").bfill().values + + # Create a closure over the computed bins which computes the power curve value for + # arbitrary array-like input + def pc_iec(x): + P = np.zeros(np.shape(x)) + for i in range(0, len(bins) - 1): + idx = np.where((x >= bins[i]) & (x < bins[i + 1])) + P[idx] = P_bin[i] + cutoff_idx = (x < windspeed_start) | (x > windspeed_end) + P[cutoff_idx] = 0.0 + return P + + return pc_iec
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/wombat/windfarm/system/cable.html b/_modules/wombat/windfarm/system/cable.html new file mode 100644 index 00000000..009063b7 --- /dev/null +++ b/_modules/wombat/windfarm/system/cable.html @@ -0,0 +1,842 @@ + + + + + + + + + + wombat.windfarm.system.cable — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for wombat.windfarm.system.cable

+""""Defines the Cable class and cable simulations."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+from itertools import zip_longest
+from collections.abc import Generator
+
+import numpy as np
+import simpy
+
+from wombat.core import (
+    Failure,
+    Maintenance,
+    RepairRequest,
+    SubassemblyData,
+    WombatEnvironment,
+)
+
+
+
+[docs] +class Cable: + """The cable system/asset class. + + Parameters + ---------- + windfarm : ``wombat.windfarm.Windfarm`` + The ``Windfarm`` object. + env : WombatEnvironment + The simulation environment. + cable_id : str + The unique identifier for the cable. + connection_type : str + The type of cable. Must be one of "array" or "export". + start_node : str + The starting point (``system.id``) (turbine or substation) of the cable segment. + cable_data : dict + The dictionary defining the cable segment. + """ + + def __init__( + self, + windfarm, + env: WombatEnvironment, + connection_type: str, + start_node: str, + end_node: str, + cable_data: dict, + name: str | None = None, + ) -> None: + """Initializes the ``Cable`` class. + + Parameters + ---------- + windfarm : ``wombat.windfarm.Windfarm`` + The ``Windfarm`` object. + env : WombatEnvironment + The simulation environment. + connection_type : str + One of "export" or "array". + cable_id : str + The unique identifier for the cable. + start_node : str + The starting point (``system.id``) (turbine or substation) of the cable + segment. + end_node : str + The ending point (``system.id``) (turbine or substation) of the cable + segment. + cable_data : dict + The dictionary defining the cable segment. + name : str | None + The name of the cable to use during logging. + """ + self.env = env + self.windfarm = windfarm + self.connection_type = connection_type + self.start_node = start_node + self.end_node = end_node + self.id = f"cable::{start_node}::{end_node}" + self.system = windfarm.system(start_node) + + if self.connection_type not in ("array", "export"): + raise ValueError( + f"Input to `connection_type` for {self.id} must be one of 'array'" + " or 'export'." + ) + + cable_data = { + **cable_data, + "system_value": self.system.value, + "rng": self.env.random_generator, + } + self.data = SubassemblyData.from_dict(cable_data) + self.name = self.data.name if name is None else name + + self.operating_level = 1.0 + self.servicing = self.env.event() + self.servicing_queue = self.env.event() + self.downstream_failure = self.env.event() + self.broken = self.env.event() + + # Ensure events start as processed and inactive + self.servicing.succeed() + self.servicing_queue.succeed() + self.downstream_failure.succeed() + self.broken.succeed() + + # TODO: need to get the time scale of a distribution like this + self.processes = dict(self._create_processes()) + +
+[docs] + def set_string_details(self, start_node: str, substation: str): + """Sets the starting turbine for the string to be used for traversing the + correct upstream connections when resetting after a failure. + + Parameters + ---------- + start_node : str + The ``System.id`` for the starting turbine on a string. + substation : str + The ``System.id`` for the string's connecting substation. + """ + self.string_start = start_node + self.substation = substation
+ + +
+[docs] + def finish_setup(self) -> None: + """Creates the ``upstream_nodes`` and ``upstream_cables`` attributes for use by + the cable when triggering usptream failures and resetting them after the repair + is complete. + """ + wf_map = self.windfarm.wind_farm_map + if self.connection_type == "array": + turbines = [self.end_node] + if self.end_node == self.string_start: + _turbines, cables = wf_map.get_upstream_connections( + self.substation, self.string_start, self.string_start + ) + else: + _turbines, cables = wf_map.get_upstream_connections( + self.substation, self.string_start, self.end_node + ) + turbines.extend(_turbines) + + if self.connection_type == "export": + turbines, cables = wf_map.get_upstream_connections_from_substation( + self.substation + ) + + self.upstream_nodes = turbines + self.upstream_cables = cables
+ + +
+[docs] + def _create_processes(self): + """Creates the processes for each of the failure and maintenance types. + + Yields + ------ + Tuple[Union[str, int], simpy.events.Process] + Creates a dictionary to keep track of the running processes within the + subassembly. + """ + for failure in self.data.failures: + desc = failure.description + yield desc, self.env.process(self.run_single_failure(failure)) + + for i, maintenance in enumerate(self.data.maintenance): + desc = maintenance.description + yield desc, self.env.process(self.run_single_maintenance(maintenance))
+ + +
+[docs] + def recreate_processes(self) -> None: + """If a cable is being reset after a replacement, then all processes are + assumed to be reset to 0, and not pick back up where they left off. + """ + self.processes = dict(self._create_processes())
+ + +
+[docs] + def interrupt_processes(self) -> None: + """Interrupts all of the running processes within the subassembly except for the + process associated with failure that triggers the catastrophic failure. + + Parameters + ---------- + subassembly : Subassembly + The subassembly that should have all processes interrupted. + """ + for _, process in self.processes.items(): + try: + process.interrupt() + except RuntimeError: + # This error occurs for the process halting all other processes. + pass
+ + +
+[docs] + def interrupt_all_subassembly_processes(self) -> None: + """Thin wrapper for ``interrupt_processes`` for consistent usage with system.""" + self.interrupt_processes()
+ + +
+[docs] + def stop_all_upstream_processes(self, failure: Failure | Maintenance) -> None: + """Stops all upstream turbines and cables from producing power by creating a + ``env.event()`` for each ``System.cable_failure`` and + ``Cable.downstream_failure``, respectively. In the case of an export cable, each + string is traversed to stop the substation and upstream turbines and cables. + + Parameters + ---------- + failure : Failre + The ``Failure`` that is causing a string shutdown. + """ + shared_logging = { + "agent": self.id, + "action": "repair request", + "reason": failure.description, + "additional": "downstream cable failure", + "request_id": failure.request_id, + } + upstream_nodes = self.upstream_nodes + upstream_cables = self.upstream_cables + if self.connection_type == "export": + # Flatten the list of lists for shutting down the upstream connections + upstream_nodes = [el for string in upstream_nodes for el in string] + upstream_cables = [el for string in upstream_cables for el in string] + + # Turn off the subation + substation = self.windfarm.system(self.end_node) + substation.cable_failure = self.env.event() + substation.interrupt_all_subassembly_processes() + self.env.log_action( + system_id=self.end_node, + system_name=substation.name, + system_ol=substation.operating_level, + part_ol=np.nan, + **shared_logging, # type: ignore + ) + + for t_id, c_id in zip_longest(upstream_nodes, upstream_cables, fillvalue=None): + if TYPE_CHECKING: + assert isinstance(t_id, str) + turbine = self.windfarm.system(t_id) + turbine.cable_failure = self.env.event() + turbine.interrupt_all_subassembly_processes() + self.env.log_action( + system_id=t_id, + system_name=turbine.name, + system_ol=turbine.operating_level, + part_ol=np.nan, + **shared_logging, # type: ignore + ) + + # If at the end of the string, skip any operations on non-existent cables + if c_id is None: + continue + cable = self.windfarm.cable(c_id) + cable.downstream_failure = self.env.event() + cable.interrupt_all_subassembly_processes() + self.env.log_action( + system_id=c_id, + system_name=cable.name, + part_id=c_id, + part_name=cable.name, + system_ol=np.nan, + part_ol=cable.operating_level, + **shared_logging, # type: ignore + )
+ + +
+[docs] + def trigger_request(self, action: Maintenance | Failure): + """Triggers the actual repair or maintenance logic for a failure or maintenance + event, respectively. + + Parameters + ---------- + action : Maintenance | Failure + The maintenance or failure event that triggers a ``RepairRequest``. + """ + which = "maintenance" if isinstance(action, Maintenance) else "repair" + self.operating_level *= 1 - action.operation_reduction + + # Automatically submit a repair request + # NOTE: mypy is not caught up with attrs yet :( + repair_request = RepairRequest( # type: ignore + system_id=self.id, + system_name=self.name, + subassembly_id=self.id, + subassembly_name=self.name, + severity_level=action.level, + details=action, + cable=True, + upstream_turbines=self.upstream_nodes, + upstream_cables=self.upstream_cables, + ) + repair_request = self.system.repair_manager.register_request(repair_request) + self.env.log_action( + system_id=self.id, + system_name=self.name, + part_id=self.id, + part_name=self.name, + system_ol=self.operating_level, + part_ol=self.operating_level, + agent=self.name, + action=f"{which} request", + reason=action.description, + additional=f"severity level {action.level}", + request_id=repair_request.request_id, + ) + + if action.operation_reduction == 1: + self.broken = self.env.event() + self.interrupt_all_subassembly_processes() + self.stop_all_upstream_processes(action) + + # Remove previously submitted requests as a replacement is required + if action.replacement: + _ = self.system.repair_manager.purge_subassembly_requests( + self.id, self.id, exclude=[repair_request.request_id] + ) + self.system.repair_manager.submit_request(repair_request)
+ + +
+[docs] + def run_single_maintenance(self, maintenance: Maintenance) -> Generator: + """Runs a process to trigger one type of maintenance request throughout the + simulation. + + Parameters + ---------- + maintenance : Maintenance + A maintenance category. + + Yields + ------ + simpy.events.Timeout + Time between maintenance requests. + """ + while True: + hours_to_next = maintenance.frequency + if hours_to_next == 0: + remainder = self.env.max_run_time - self.env.now + try: + yield self.env.timeout(remainder) + except simpy.Interrupt: + remainder -= self.env.now + else: + while hours_to_next > 0: + start = -1 # Ensure an interruption before processing is caught + try: + # If the replacement has not been completed, then wait + yield self.servicing & self.downstream_failure & self.broken + + start = self.env.now + yield self.env.timeout(hours_to_next) + hours_to_next = 0 + self.trigger_request(maintenance) + except simpy.Interrupt: + if not self.broken.triggered: + # The subassembly had to restart the maintenance cycle + hours_to_next = 0 + else: + # A different process failed, so subtract the elapsed time + # only if it had started to be processed + hours_to_next -= 0 if start == -1 else self.env.now - start
+ + +
+[docs] + def run_single_failure(self, failure: Failure) -> Generator: + """Runs a process to trigger one type of failure repair request throughout the + simulation. + + Parameters + ---------- + failure : Failure + A failure classification. + + Yields + ------ + simpy.events.Timeout + Time between failure events that need to request a repair. + """ + while True: + hours_to_next = failure.hours_to_next_failure() + if hours_to_next is None: + remainder = self.env.max_run_time - self.env.now + try: + yield self.env.timeout(remainder) + except simpy.Interrupt: + remainder -= self.env.now + + else: + if TYPE_CHECKING: + assert isinstance(hours_to_next, (int, float)) # mypy helper + while hours_to_next > 0: # type: ignore + start = -1 # Ensure an interruption before processing is caught + try: + yield self.servicing & self.downstream_failure & self.broken + + start = self.env.now + yield self.env.timeout(hours_to_next) + hours_to_next = 0 + self.trigger_request(failure) + except simpy.Interrupt: + if not self.broken.triggered: + # Restart after fixing + hours_to_next = 0 + else: + # A different process failed, so subtract the elapsed time + # only if it had started to be processed + hours_to_next -= 0 if start == -1 else self.env.now - start
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/wombat/windfarm/system/subassembly.html b/_modules/wombat/windfarm/system/subassembly.html new file mode 100644 index 00000000..3d5adacb --- /dev/null +++ b/_modules/wombat/windfarm/system/subassembly.html @@ -0,0 +1,689 @@ + + + + + + + + + + wombat.windfarm.system.subassembly — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for wombat.windfarm.system.subassembly

+"""Provides the Subassembly class."""
+
+from __future__ import annotations
+
+from collections.abc import Generator
+
+import simpy
+
+from wombat.core import (
+    Failure,
+    Maintenance,
+    RepairRequest,
+    SubassemblyData,
+    WombatEnvironment,
+)
+
+
+
+[docs] +class Subassembly: + """A major system composes the turbine or substation objects.""" + + def __init__( + self, + system, + env: WombatEnvironment, + s_id: str, + subassembly_data: dict, + ) -> None: + """Creates a subassembly object that models various maintenance and failure + types. + + Parameters + ---------- + system : wombat.windfarm.System + The system containing subassembly object(s). + env : WombatEnvironment + The simulation environment. + s_id : str + A unique identifier for the subassembly within the system. + subassembly_data : dict + A dictionary to be passed to ``SubassemblyData`` for creation and + validation. + """ + self.env = env + self.system = system + self.id = s_id + + subassembly_data = { + **subassembly_data, + "system_value": self.system.value, + "rng": self.env.random_generator, + } + self.data = SubassemblyData.from_dict(subassembly_data) + self.name = self.data.name + + self.operating_level = 1.0 + self.broken = self.env.event() + self.broken.succeed() # start the event as inactive + + self.processes = dict(self._create_processes()) + +
+[docs] + def _create_processes(self): + """Creates the processes for each of the failure and maintenance types. + + Yields + ------ + Tuple[Union[str, int], simpy.events.Process] + Creates a dictionary to keep track of the running processes within the + subassembly. + """ + for failure in self.data.failures: + level = failure.level + desc = failure.description + yield (level, desc), self.env.process(self.run_single_failure(failure)) + + for maintenance in self.data.maintenance: + desc = maintenance.description + yield desc, self.env.process(self.run_single_maintenance(maintenance))
+ + +
+[docs] + def recreate_processes(self) -> None: + """If a turbine is being reset after a tow-to-port repair or replacement, then + all processes are assumed to be reset to 0, and not pick back up where they left + off. + """ + self.processes = dict(self._create_processes())
+ + +
+[docs] + def interrupt_processes(self, origin: Subassembly | None = None) -> None: + """Interrupts all of the running processes within the subassembly except for the + process associated with failure that triggers the catastrophic failure. + + Parameters + ---------- + origin : Subassembly + The subassembly that triggered the request, if the method call is coming + from a subassembly shutdown event. If provided, and it is the same as the + current subassembly, then a try/except flow is used to ensure the process + that initiated the shutdown is not interrupting itself. + """ + if origin is not None and id(origin) == id(self): + for _, process in self.processes.items(): + try: + process.interrupt() + except RuntimeError: # Process initiating process can't be interrupted + pass + return + + for _, process in self.processes.items(): + process.interrupt()
+ + +
+[docs] + def interrupt_all_subassembly_processes(self) -> None: + """Thin wrapper for ``system.interrupt_all_subassembly_processes``.""" + self.system.interrupt_all_subassembly_processes(origin=self)
+ + +
+[docs] + def trigger_request(self, action: Maintenance | Failure): + """Triggers the actual repair or maintenance logic for a failure or maintenance + event, respectively. + + Parameters + ---------- + action : Maintenance | Failure + The maintenance or failure event that triggers a ``RepairRequest``. + """ + which = "maintenance" if isinstance(action, Maintenance) else "repair" + self.operating_level *= 1 - action.operation_reduction + if action.operation_reduction == 1: + self.broken = self.env.event() + self.interrupt_all_subassembly_processes() + + # Remove previously submitted requests if a replacement is required + if action.replacement: + _ = self.system.repair_manager.purge_subassembly_requests( + self.system.id, self.id + ) + + # Automatically submit a repair request + # NOTE: mypy is not caught up with attrs yet :( + repair_request = RepairRequest( # type: ignore + system_id=self.system.id, + system_name=self.system.name, + subassembly_id=self.id, + subassembly_name=self.name, + severity_level=action.level, + details=action, + ) + repair_request = self.system.repair_manager.register_request(repair_request) + self.env.log_action( + system_id=self.system.id, + system_name=self.system.name, + part_id=self.id, + part_name=self.name, + system_ol=self.system.operating_level, + part_ol=self.operating_level, + agent=self.name, + action=f"{which} request", + reason=action.description, + additional=f"severity level {action.level}", + request_id=repair_request.request_id, + ) + self.system.repair_manager.submit_request(repair_request)
+ + +
+[docs] + def run_single_maintenance(self, maintenance: Maintenance) -> Generator: + """Runs a process to trigger one type of maintenance request throughout the + simulation. + + Parameters + ---------- + maintenance : Maintenance + A maintenance category. + + Yields + ------ + simpy.events. HOURS_IN_DAY + Time between maintenance requests. + """ + while True: + hours_to_next = maintenance.frequency + if hours_to_next == 0: + remainder = self.env.max_run_time - self.env.now + try: + yield self.env.timeout(remainder) + except simpy.Interrupt: + remainder -= self.env.now + else: + while hours_to_next > 0: + start = -1 # Ensure an interruption before processing is caught + try: + # Wait until these events are triggered and back to operational + yield ( + self.system.servicing + & self.system.cable_failure + & self.broken + ) + + start = self.env.now + yield self.env.timeout(hours_to_next) + hours_to_next = 0 + self.trigger_request(maintenance) + + except simpy.Interrupt: + if not self.broken.triggered: + # The subassembly had to restart the maintenance cycle + hours_to_next = 0 + else: + # A different process failed, so subtract the elapsed time + # only if it had started to be processed + hours_to_next -= 0 if start == -1 else self.env.now - start
+ + +
+[docs] + def run_single_failure(self, failure: Failure) -> Generator: + """Runs a process to trigger one type of failure repair request throughout the + simulation. + + Parameters + ---------- + failure : Failure + A failure classification. + + Yields + ------ + simpy.events. HOURS_IN_DAY + Time between failure events that need to request a repair. + """ + while True: + hours_to_next = failure.hours_to_next_failure() + if hours_to_next is None: + remainder = self.env.max_run_time - self.env.now + try: + yield self.env.timeout(remainder) + except simpy.Interrupt: + remainder -= self.env.now + continue + else: + while hours_to_next > 0: # type: ignore + start = -1 # Ensure an interruption before processing is caught + try: + yield ( + self.system.servicing + & self.system.cable_failure + & self.broken + ) + start = self.env.now + yield self.env.timeout(hours_to_next) + hours_to_next = 0 + self.trigger_request(failure) + + except simpy.Interrupt: + if not self.broken.triggered: + # The subassembly had to be replaced so reset the timing + hours_to_next = 0 + else: + # A different process failed, so subtract the elapsed time + # only if it had started to be processed + hours_to_next -= 0 if start == -1 else self.env.now - start
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/wombat/windfarm/system/system.html b/_modules/wombat/windfarm/system/system.html new file mode 100644 index 00000000..b54d8774 --- /dev/null +++ b/_modules/wombat/windfarm/system/system.html @@ -0,0 +1,654 @@ + + + + + + + + + + wombat.windfarm.system.system — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for wombat.windfarm.system.system

+"""Creates the Turbine class."""
+from __future__ import annotations
+
+from typing import Callable
+from operator import mul
+from functools import reduce
+
+import numpy as np
+import pandas as pd
+
+from wombat.core import RepairManager, WombatEnvironment
+from wombat.utilities import IEC_power_curve
+from wombat.windfarm.system import Subassembly
+from wombat.utilities.utilities import create_variable_from_string
+
+
+
+[docs] +class System: + """Can either be a turbine or substation, but is meant to be something that consists + of 'Subassembly' pieces. + + See `here <https://www.sciencedirect.com/science/article/pii/S1364032117308985>`_ + for more information. + """ + + def __init__( + self, + env: WombatEnvironment, + repair_manager: RepairManager, + t_id: str, + name: str, + subassemblies: dict, + system: str, + ): + """Initializes an individual windfarm asset. + + Parameters + ---------- + env : WombatEnvironment + The simulation environment. + repair_manager : RepairManager + The simulation repair and maintenance task manager. + t_id : str + The unique identifier for the asset. + name : str + The long form name/descriptor for the system/asset. + subassemblies : dict + The dictionary of subassemblies required for the system/asset. + system : str + The identifier should be one of "turbine" or "substation" to indicate the + type of system this will be. + + Raises + ------ + ValueError + [description] + """ + self.env = env + self.repair_manager = repair_manager + self.id = t_id + self.name = name + self.capacity = subassemblies["capacity_kw"] + self.subassemblies: list[Subassembly] = [] + self.servicing = self.env.event() + self.servicing_queue = self.env.event() + self.cable_failure = self.env.event() + + # Ensure servicing statuses starts as processed and inactive + self.servicing.succeed() + self.servicing_queue.succeed() + self.cable_failure.succeed() + + system = system.lower().strip() + self._calculate_system_value(subassemblies) + if system not in ("turbine", "substation"): + raise ValueError("'system' must be one of 'turbine' or 'substation'!") + + self._create_subassemblies(subassemblies, system) + +
+[docs] + def _calculate_system_value(self, subassemblies: dict) -> None: + """Calculates the turbine's value based its capex_kw and capacity. + + Parameters + ---------- + system : str + One of "turbine" or "substation". + subassemblies : dict + Dictionary of subassemblies. + """ + self.value = subassemblies["capacity_kw"] * subassemblies["capex_kw"]
+ + +
+[docs] + def _create_subassemblies(self, subassembly_data: dict, system: str) -> None: + """Creates each subassembly as a separate attribute and also a list for quick + access. + + Parameters + ---------- + subassembly_data : dict + Dictionary providing the maintenance and failure definitions for at least + one subassembly named + system : str + One of "turbine" or "substation" to indicate if the power curves should also + be created, or not. + """ + # Set the subassembly data variables from the remainder of the keys in the + # system configuration file/dictionary + exclude_keys = ["capacity_kw", "capex_kw", "power_curve"] + for key, data in subassembly_data.items(): + if key in exclude_keys: + continue + name = create_variable_from_string(key) + subassembly = Subassembly(self, self.env, name, data) + setattr(self, name, subassembly) + self.subassemblies.append(getattr(self, name)) + + if self.subassemblies == []: + raise ValueError( + "At least one subassembly definition requred for ", + f"ID: {self.id}, Name: {self.name}.", + ) + + self.env.log_action( + agent=self.name, + action=f"subassemblies created: {[s.id for s in self.subassemblies]}", + reason="windfarm initialization", + system_id=self.id, + system_name=self.name, + system_ol=self.operating_level, + part_ol=1, + additional="initialization", + ) + + # If the system is a turbine, create the power curve, if available + if system == "turbine": + self._initialize_power_curve(subassembly_data.get("power_curve", None))
+ + +
+[docs] + def _initialize_power_curve(self, power_curve_dict: dict | None) -> None: + """Creates the power curve function based on the ``power_curve`` input in the + ``subassembly_data`` dictionary. If there is no valid input, then 0 will always + be reutrned. + + Parameters + ---------- + power_curve_dict : dict + The turbine definition dictionary. + """ + self.power_curve: Callable + if power_curve_dict is None: + self.power_curve = IEC_power_curve(pd.Series([0]), pd.Series([0])) + else: + power_curve_file = self.env.data_dir / "turbines" / power_curve_dict["file"] + power_curve = pd.read_csv(power_curve_file) + power_curve = power_curve.loc[power_curve.power_kw != 0].reset_index( + drop=True + ) + bin_width = power_curve_dict.get("bin_width", 0.5) + self.power_curve = IEC_power_curve( + power_curve.windspeed_ms, + power_curve.power_kw, + windspeed_start=power_curve.windspeed_ms.min(), + windspeed_end=power_curve.windspeed_ms.max(), + bin_width=bin_width, + )
+ + +
+[docs] + def interrupt_all_subassembly_processes( + self, origin: Subassembly | None = None + ) -> None: + """Interrupts the running processes in all of the system's subassemblies. + + Parameters + ---------- + origin : Subassembly + The subassembly that triggered the request, if the method call is coming + from a subassembly shutdown event. + """ + [ + subassembly.interrupt_processes(origin=origin) # type: ignore + for subassembly in self.subassemblies + ]
+ + + @property + def operating_level(self) -> float: + """The turbine's operating level, based on subassembly and cable performance. + + Returns + ------- + float + Operating level of the turbine. + """ + if self.cable_failure.triggered and self.servicing.triggered: + ol: float = reduce(mul, [sub.operating_level for sub in self.subassemblies]) + return ol # type: ignore + return 0.0 + + @property + def operating_level_wo_servicing(self) -> float: + """The turbine's operating level, based on subassembly and cable performance, + without accounting for servicing status. + + Returns + ------- + float + Operating level of the turbine. + """ + if self.cable_failure.triggered: + ol: float = reduce(mul, [sub.operating_level for sub in self.subassemblies]) + return ol # type: ignore + return 0.0 + +
+[docs] + def power(self, windspeed: list[float] | np.ndarray) -> np.ndarray: + """Generates the power output for an iterable of windspeed values. + + Parameters + ---------- + windspeed : list[float] | np.ndarrays + Windspeed values, in m/s. + + Returns + ------- + np.ndarray + Power production, in kW. + """ + return self.power_curve(windspeed)
+
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_modules/wombat/windfarm/windfarm.html b/_modules/wombat/windfarm/windfarm.html new file mode 100644 index 00000000..cb42ade2 --- /dev/null +++ b/_modules/wombat/windfarm/windfarm.html @@ -0,0 +1,965 @@ + + + + + + + + + + wombat.windfarm.windfarm — WOMBAT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+
+
+ + + + +
+
+ + + + + +
+ + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ +
+
+ + + +
+

+ +
+
+ +
+
+
+ + + + +
+ +

Source code for wombat.windfarm.windfarm

+"""Creates the Windfarm class/model."""
+from __future__ import annotations
+
+import csv
+import datetime as dt
+import itertools
+from math import fsum
+
+import numpy as np
+import pandas as pd
+import networkx as nx
+from geopy import distance
+
+from wombat.core import RepairManager, WombatEnvironment
+from wombat.core.library import load_yaml
+from wombat.windfarm.system import Cable, System
+from wombat.core.data_classes import String, SubString, WindFarmMap, SubstationMap
+from wombat.utilities.utilities import cache
+
+
+
+[docs] +class Windfarm: + """The primary class for operating on objects within a windfarm. The substations, + cables, and turbines are created as a network object to be more appropriately + accessed and controlled. + """ + + def __init__( + self, + env: WombatEnvironment, + windfarm_layout: str, + repair_manager: RepairManager, + ) -> None: + self.env = env + self.repair_manager = repair_manager + + # Set up the layout and instantiate all windfarm objects + self.configs: dict[str, dict] = {"turbine": {}, "substation": {}, "cable": {}} + self._create_graph_layout(windfarm_layout) + self._create_turbines_and_substations() + self._create_cables() + self.capacity: int | float = sum( + self.system(turb).capacity for turb in self.turbine_id + ) + self._create_substation_turbine_map() + self._create_wind_farm_map() + self.finish_setup() + self.calculate_distance_matrix() + + # Create the logging items + self.system_list = list(self.graph.nodes) + self._setup_logger() + + # Register the windfarm and start the logger + self.repair_manager._register_windfarm(self) + self.env._register_windfarm(self) + self.env.process(self._log_operations()) + +
+[docs] + def _create_graph_layout(self, windfarm_layout: str) -> None: + """Creates a network layout of the windfarm start from the substation(s) to + be able to capture downstream turbines that can be cut off in the event of a + cable failure. + + Parameters + ---------- + windfarm_layout : str + Filename to use for reading in the windfarm layout; must be a csv file. + """ + layout_path = str(self.env.data_dir / "project/plant" / windfarm_layout) + layout = ( + pd.read_csv(layout_path) + .sort_values(by=["string", "order"]) + .reset_index(drop=True) + ) + layout.subassembly = layout.subassembly.fillna("") + layout.upstream_cable = layout.upstream_cable.fillna("") + + windfarm = nx.DiGraph() + windfarm.add_nodes_from(layout.id.values) + + # Assign the data attributes to the graph nodes + for col in ("name", "latitude", "longitude", "subassembly"): + nx.set_node_attributes(windfarm, dict(layout[["id", col]].values), name=col) + + # Determine which nodes are substations and which are turbines + if "type" in layout.columns: + if layout.loc[~layout.type.isin(("substation", "turbine"))].size > 0: + raise ValueError( + "At least one value in the 'type' column are not one of:" + " 'substation' or 'turbine'." + ) + substation_filter = layout.type == "substation" + nx.set_node_attributes( + windfarm, dict(layout[["id", "type"]].values), name="type" + ) + else: + substation_filter = layout.id == layout.substation_id + _type = {True: "substation", False: "turbine"} + d = {i: _type[val] for i, val in zip(layout.id, substation_filter.values)} + nx.set_node_attributes(windfarm, d, name="type") + + self.substation_id = layout.loc[substation_filter, "id"].values + for substation in self.substation_id: + windfarm.nodes[substation]["connection"] = layout.loc[ + layout.id == substation, "substation_id" + ].values[0] + + self.turbine_id: np.ndarray = layout.loc[~substation_filter, "id"].values + substations = layout[substation_filter].copy() + turbines = layout[~substation_filter].copy() + substation_sections = [ + turbines[turbines.substation_id == substation] + for substation in substations.id + ] + for section in substation_sections: + for _, row in section.iterrows(): + if row.order == 0: + start: str = row.substation_id + else: + start = current # noqa: F821 + current: str = row.id + windfarm.add_edge( + start, current, length=row.distance, cable=row.upstream_cable + ) + for substation in self.substation_id: + row = layout.loc[layout.id == substation] + windfarm.add_edge( + row.substation_id.values[0], + substation, + length=row.distance.values[0], + cable=row.upstream_cable.values[0], + ) + + self.graph: nx.DiGraph = windfarm + self.layout_df = layout
+ + +
+[docs] + def _create_turbines_and_substations(self) -> None: + """Instantiates the turbine and substation models as defined in the + user-provided layout file, and connects these models to the appropriate graph + nodes to create a fully representative windfarm network model. + + Raises + ------ + ValueError + Raised if the subassembly data is not provided in the layout file. + """ + for system_id, data in self.graph.nodes(data=True): + name = data["subassembly"] + node_type = data["type"] + if name == "": + raise ValueError( + "A 'subassembly' file must be specified for all nodes in the" + " windfarm layout!" + ) + + if (subassembly_dict := self.configs[node_type].get(name)) is None: + subassembly_dict = load_yaml(self.env.data_dir / f"{node_type}s", name) + self.configs[node_type][name] = subassembly_dict + + self.graph.nodes[system_id]["system"] = System( + self.env, + self.repair_manager, + system_id, + data["name"], + subassembly_dict, + node_type, + )
+ + +
+[docs] + def _create_cables(self) -> None: + """Instantiates the cable models as defined in the user-provided layout file, + and connects these models to the appropriate graph edges to create a fully + representative windfarm network model. + + Raises + ------ + ValueError + Raised if the cable model is not specified. + """ + get_name = "upstream_cable_name" in self.layout_df + bad_data_location_messages = [] + for start_node, end_node, data in self.graph.edges(data=True): + name = data["cable"] + + # Check that the cable data is provided + if name == "": + raise ValueError( + "An 'upstream_cable' file must be specified for all nodes in the" + " windfarm layout!" + ) + if (cable_dict := self.configs["cable"].get(name)) is None: + try: + cable_dict = load_yaml(self.env.data_dir / "cables", data["cable"]) + except FileNotFoundError: + cable_dict = load_yaml( + self.env.data_dir / "windfarm", data["cable"] + ) + bad_data_location_messages.append( + "In v0.7, all cable configurations must be located in:" + " '<library>/cables/" + ) + self.configs["cable"][name] = cable_dict + + start_coordinates = ( + self.graph.nodes[start_node]["latitude"], + self.graph.nodes[start_node]["longitude"], + ) + end_coordinates = ( + self.graph.nodes[end_node]["latitude"], + self.graph.nodes[end_node]["longitude"], + ) + + name = None + if get_name: + name, *_ = self.layout_df.loc[ + self.layout_df.id == end_node, "upstream_cable_name" + ] + + # If the real distance/cable length is not input, then the geodesic distance + # is calculated + if data["length"] == 0: + data["length"] = distance.geodesic( + start_coordinates, end_coordinates, ellipsoid="WGS-84" + ).km + + if self.graph.nodes[end_node]["type"] == "substation": + data["type"] = "export" + else: + data["type"] = "array" + + data["cable"] = Cable( + self, self.env, data["type"], start_node, end_node, cable_dict, name + ) + + # Calaculate the geometric center point + end_points = np.array((start_coordinates, end_coordinates)) + data["latitude"], data["longitude"] = end_points.mean(axis=0)
+ + +
+[docs] + def calculate_distance_matrix(self) -> None: + """Calculates the geodesic distance, in km, between all of the windfarm's nodes, + e.g., substations and turbines, and cables. + """ + ids = list(self.graph.nodes()) + ids.extend([data["cable"].id for *_, data in self.graph.edges(data=True)]) + coords = [ + (data["latitude"], data["longitude"]) + for *_, data in (*self.graph.nodes(data=True), *self.graph.edges(data=True)) + ] + + dist = [ + distance.geodesic(c1, c2).km for c1, c2 in itertools.combinations(coords, 2) + ] + dist_arr = np.ones((len(ids), len(ids))) + triangle_ix = np.triu_indices_from(dist_arr, 1) + dist_arr[triangle_ix] = dist_arr.T[triangle_ix] = dist + + # Set the self distance to infinity, so that only one crew can be dropped off + # at a single point + np.fill_diagonal(dist_arr, np.inf) + + self.distance_matrix = pd.DataFrame(dist_arr, index=ids, columns=ids)
+ + +
+[docs] + def _create_substation_turbine_map(self) -> None: + """Creates ``substation_turbine_map``, a dictionary, that maps substation(s) to + the dependent turbines in the windfarm, and the weighting of each turbine in the + windfarm. + """ + # Get all turbines connected to each substation, excepting any connected via + # export cables that connect substations as these operate independently + s_t_map: dict = {s: {"turbines": [], "weights": []} for s in self.substation_id} + for substation_id in self.substation_id: + nodes = set( + nx.bfs_tree(self.graph, substation_id, depth_limit=1).nodes + ).difference(self.substation_id) + for node in list(nodes): + nodes.update( + list(itertools.chain(*nx.dfs_successors(self.graph, node).values())) + ) + s_t_map[substation_id]["turbines"] = np.array(list(nodes)) + + # Reorient the mapping to have the turbine list and the capacity-based weighting + # of each turbine + for s_id in s_t_map: + s_t_map[s_id]["weights"] = ( + np.array([self.system(t).capacity for t in s_t_map[s_id]["turbines"]]) + / self.capacity + ) + + self.substation_turbine_map: dict[str, dict[str, np.ndarray]] = s_t_map + + # Calculate the turbine weights + self.turbine_weights: pd.DataFrame = ( + pd.concat([pd.DataFrame(val) for val in s_t_map.values()]) + .set_index("turbines") + .T + )
+ + +
+[docs] + def _create_wind_farm_map(self) -> None: + """Creates a secondary graph object strictly for traversing the windfarm to turn + on/off the appropriate turbines, substations, and cables more easily. + """ + substations = self.substation_id + graph = self.graph + wind_map = dict(zip(substations, itertools.repeat({}))) + + export = [el for el in graph.edges if el[1] in substations] + for cable_tuple in export: + self.cable(cable_tuple).set_string_details(*cable_tuple[::-1]) + + for s_id in self.substation_id: + start_nodes = list( + set(nx.bfs_tree(graph, s_id, depth_limit=1).nodes).difference( + substations + ) + ) + wind_map[s_id] = {"strings": dict(zip(start_nodes, itertools.repeat({})))} + + for start_node in start_nodes: + upstream = list( + itertools.chain(*nx.dfs_successors(graph, start_node).values()) + ) + wind_map[s_id]["strings"][start_node] = { + start_node: SubString( + downstream=s_id, # type: ignore + upstream=upstream, # type: ignore + ) + } + self.cable((s_id, start_node)).set_string_details(start_node, s_id) + + downstream = start_node + for node in upstream: + wind_map[s_id]["strings"][start_node][node] = SubString( + downstream=downstream, # type: ignore + upstream=list( # tye: ignore + itertools.chain(*nx.dfs_successors(graph, node).values()) + ), + ) + self.cable((downstream, node)).set_string_details(start_node, s_id) + downstream = node + + wind_map[s_id]["strings"][start_node] = String( # type: ignore + start=start_node, upstream_map=wind_map[s_id]["strings"][start_node] + ) + wind_map[s_id] = SubstationMap( # type: ignore + string_starts=start_nodes, + string_map=wind_map[s_id]["strings"], + downstream=graph.nodes[s_id]["connection"], + ) + self.wind_farm_map = WindFarmMap( + substation_map=wind_map, + export_cables=export, # type: ignore + )
+ + +
+[docs] + def finish_setup(self) -> None: + """Final initialization hook for any substations, turbines, or cables.""" + for start_node, end_node in self.graph.edges(): + self.cable((start_node, end_node)).finish_setup()
+ + +
+[docs] + def _setup_logger(self, initial: bool = True): + self._log_columns = [ + "datetime", + "env_datetime", + "env_time", + ] + self.system_list + + self.env._operations_writer = csv.DictWriter( + self.env._operations_csv, delimiter="|", fieldnames=self._log_columns + ) + if initial: + self.env._operations_writer.writeheader()
+ + +
+[docs] + def _log_operations(self): + """Logs the operational data for a simulation.""" + message = { + "datetime": dt.datetime.now(), + "env_datetime": self.env.simulation_time, + "env_time": self.env.now, + } + message.update( + {system: self.system(system).operating_level for system in self.system_list} + ) + self.env._operations_writer.writerow(message) + + HOURS = 1 + while True: + for _ in range(10000): + yield self.env.timeout(HOURS) + message = { + "datetime": dt.datetime.now(), + "env_datetime": self.env.simulation_time, + "env_time": self.env.now, + } + message.update( + { + system: self.system(system).operating_level + for system in self.system_list + } + ) + self.env._operations_buffer.append(message) + self.env._operations_writer.writerows(self.env._operations_buffer) + self.env._operations_buffer.clear()
+ + +
+[docs] + @cache + def system(self, system_id: str) -> System: + """Convenience function to returns the desired `System` object for a turbine or + substation in the windfarm. + + Parameters + ---------- + system_id : str + The system's unique identifier, ``wombat.windfarm.System.id``. + + Returns + ------- + System + The ``System`` object. + """ + return self.graph.nodes[system_id]["system"]
+ + +
+[docs] + @cache + def cable(self, cable_id: tuple[str, str] | str) -> Cable: + """Convenience function to returns the desired `Cable` object for a cable in the + windfarm. + + Parameters + ---------- + cable_id : tuple[str, str] | str + The cable's unique identifier, of the form: (``wombat.windfarm.System.id``, + ``wombat.windfarm.System.id``), for the (downstream node id, upstream node + id), or the ``Cable.id``. + + Returns + ------- + Cable + The ``Cable`` object. + """ + if isinstance(cable_id, str): + edge_id = tuple(cable_id.split("::")) + if len(edge_id) == 3: + edge_id = edge_id[1:] + else: + edge_id = cable_id + try: + return self.graph.edges[edge_id]["cable"] + except KeyError: + raise KeyError(f"Edge {edge_id} is invalid.")
+ + + @property + def current_availability(self) -> float: + r"""Calculates the product of all system ``operating_level`` variables across + the windfarm using the following forumation. + + .. math:: + \sum{ + OperatingLevel_{substation_{i}} * + \sum{OperatingLevel_{turbine_{j}} * Weight_{turbine_{j}}} + } + + where the :math:``{OperatingLevel}`` is the product of the operating level + of each subassembly on a given system (substation or turbine), and the + :math:``{Weight}`` is the proportion of one turbine's capacity relative to + the whole windfarm. + """ # noqa: W605 + operating_levels = { + s_id: [ + self.system(t).operating_level + for t in self.substation_turbine_map[s_id]["turbines"] # type: ignore + ] + for s_id in self.substation_turbine_map + } + availability = fsum( + [ + self.system(s_id).operating_level + * fsum( + operating_levels[s_id] + * self.substation_turbine_map[s_id]["weights"] # type: ignore + ) + for s_id in self.substation_turbine_map + ] + ) + return availability + + @property + def current_availability_wo_servicing(self) -> float: + r"""Calculates the product of all system ``operating_level`` variables across + the windfarm using the following forumation, ignoring 0 operating level due to + ongoing servicing. + + .. math:: + \sum{ + OperatingLevel_{substation_{i}} * + \sum{OperatingLevel_{turbine_{j}} * Weight_{turbine_{j}}} + } + + where the :math:``{OperatingLevel}`` is the product of the operating level + of each subassembly on a given system (substation or turbine), and the + :math:``{Weight}`` is the proportion of one turbine's capacity relative to + the whole windfarm. + """ # noqa: W605 + operating_levels = { + s_id: [ + self.system(t).operating_level_wo_servicing + for t in self.substation_turbine_map[s_id]["turbines"] # type: ignore + ] + for s_id in self.substation_turbine_map + } + availability = fsum( + [ + self.system(s_id).operating_level_wo_servicing + * fsum( + operating_levels[s_id] + * self.substation_turbine_map[s_id]["weights"] # type: ignore + ) + for s_id in self.substation_turbine_map + ] + ) + return availability
+ +
+ +
+ + + + + + +
+ +
+
+
+ +
+ + + +
+ + +
+ + + +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/_sources/API/core.md b/_sources/API/core.md new file mode 100644 index 00000000..2d80a44f --- /dev/null +++ b/_sources/API/core.md @@ -0,0 +1,64 @@ +(core)= +# Simulation Core Classes + +There are a variety of components that enable a simulation to be run, from the +environment to the management of repairs to the servicing equipment. The below will show +how each of the APIs are powered to enable the full flexibility of modeling. + +```{image} ../images/simulation_tools.svg +:alt: +:align: center +:width: 2400px +``` + +(core:environment)= +## Environment + +```{eval-rst} +.. autoclass:: wombat.core.environment.WombatEnvironment + :members: + :undoc-members: +``` + +(core:repair-manager)= +## Repair Management + +```{eval-rst} +.. autoclass:: wombat.core.repair_management.RepairManager + :members: + :undoc-members: + :exclude-members: _current_id +``` + +(core:service-equipment)= +## Servicing Equipment + +```{eval-rst} +.. automodule:: wombat.core.service_equipment + :members: + :inherited-members: + :exclude-members: ServiceEquipment + +.. autoclass:: wombat.core.service_equipment.ServiceEquipment + :members: + :inherited-members: + :undoc-members: + :exclude-members: env, windfarm, manager, settings, port +``` + +(core:port)= +## Port + +```{eval-rst} +.. automodule:: wombat.core.port + :exclude-members: Port + +.. autoclass:: wombat.core.port.Port + :members: transfer_requests_from_manager, repair_single, run_repairs, + wait_until_next_shift, run_tow_to_port, run_unscheduled_in_situ + :inherited-members: + :undoc-members: + :exclude-members: env, windfarm, manager, settings, requests_serviced, turbine_manager, + crew_manager, tugboat_manager, active_repairs, GetQueue, PutQueue, capacity, get, + get_queue, items, put, put_queue +``` diff --git a/_sources/API/index.md b/_sources/API/index.md new file mode 100644 index 00000000..6bf663d1 --- /dev/null +++ b/_sources/API/index.md @@ -0,0 +1,21 @@ +# WOMBAT API + +The WOMBAT framework relies on a set of base data classes powered by the attrs +library and a series of simulation classes and methods to perform all the operations. + +To make it easier for users, there is also a simulation interface provided. + +## Package Hierarchy + +```{image} ../images/package_hierarchy.svg +:alt: +:align: center +``` + +## Class Hierarchy + +```{image} ../images/class_diagram.svg +:alt: +:align: center +:width: 2400px +``` diff --git a/_sources/API/simulation_api.md b/_sources/API/simulation_api.md new file mode 100644 index 00000000..c9652daa --- /dev/null +++ b/_sources/API/simulation_api.md @@ -0,0 +1,51 @@ +(simulation-api)= +# Simulation API + +```{image} ../images/simulation_api.svg +:alt: +:align: center +:width: 2400px +``` + +(simulation-api:config)= +## Configuration + +```{eval-rst} +.. autoclass:: wombat.core.simulation_api.Configuration + :members: + :undoc-members: + :exclude-members: name, library, layout, service_equipment, weather, workday_start, + workday_end, inflation_rate, fixed_costs, project_capacity, start_year, end_year, + port, port_distance, non_operational_start, non_operational_end, reduced_speed_start, + reduced_speed_end, reduced_speed, random_seed, random_generator +``` + +(simulation-api:interface)= +## Simulation Interface + +```{eval-rst} +.. autoclass:: wombat.core.simulation_api.Simulation + :members: + :undoc-members: + :exclude-members: setup_simulation, config, env, initialize_metrics, library_path, + metrics, repair_manager, service_equipment, windfarm, port, random_seed, + random_generator +``` + +(simulation-api:metrics)= +## Metrics Computation + +For example usage of the Metrics class and its associated methods, please see the +[examples documentation page](../examples/metrics_demonstration) + +```{eval-rst} +.. autoclass:: wombat.core.post_processor.Metrics + :members: from_simulation_outputs, power_production, time_based_availability, + production_based_availability, capacity_factor, task_completion_rate, + equipment_costs, service_equipment_utilization, vessel_crew_hours_at_sea, + number_of_tows, labor_costs, equipment_labor_cost_breakdowns, emissions, + process_times, component_costs, port_fees, project_fixed_costs, opex, npv + :member-order: bysource + :undoc-members: + :exclude-members: +``` diff --git a/_sources/API/types.md b/_sources/API/types.md new file mode 100644 index 00000000..d8c2340f --- /dev/null +++ b/_sources/API/types.md @@ -0,0 +1,200 @@ +(types)= +# Data Classes + +The WOMBAT architecture relies heavily on a base set of data classes to process most of +the model's inputs. This enables a rigid, yet robust data model to properly define a +simulation. + +```{image} ../images/data_classes.svg +:alt: +:align: center +:width: 2400px +``` + +## What is this `FromDictMixin` I keep seeing in the code diagrams? + +The `FromDictMixin` class provides a standard method for providing dictionary definitions +to `attrs` dataclasses without worrying about overloading the definition. Plus, you get +the convenience of writing `cls.from_dict(data_dict)` instead of cls(**data_dict), and +hoping for the best. + +```{eval-rst} +.. autoclass:: wombat.core.data_classes.FromDictMixin + :members: + :undoc-members: + :exclude-members: +``` + +(types:maintenance)= +## Scheduled and Unscheduled Maintenance + +(types:maintenance:scheduled)= +### Maintenance Tasks + +```{eval-rst} +.. autoclass:: wombat.core.data_classes.Maintenance + :members: + :undoc-members: + :exclude-members: time, materials, frequency, equipment, system_value, description, + level, operation_reduction, rng, service_equipment, replacement +``` + +(types:maintenance:unscheduled)= +### Failures + +```{eval-rst} +.. autoclass:: wombat.core.data_classes.Failure + :members: + :undoc-members: + :exclude-members: scale, shape, time, materials, operation_reduction, weibull, name, + maintenance, failures, level, equipment, system_value, description, rng, + service_equipment, replacement +``` + +(types:maintenance:requests)= +### Repair Requests + +```{eval-rst} +.. autoclass:: wombat.core.data_classes.RepairRequest + :members: + :undoc-members: + :exclude-members: system_id, system_name, subassembly_id, subassembly_name, + severity_level, details, cable, upstream_turbines, upstream_cables, +``` + +(types:service-equipment)= +## Servicing Equipment and Crews + +### Service Equipment + +```{eval-rst} +.. autoclass:: wombat.core.data_classes.ServiceEquipmentData + :members: + :undoc-members: + :exclude-members: data_dict, strategy +``` + +### ServiceCrew + +```{eval-rst} +.. autoclass:: wombat.core.data_classes.ServiceCrew + :members: + :undoc-members: + :exclude-members: n_day_rate, day_rate, n_hourly_rate, hourly_rate +``` + +(types:service-equipment:scheduled)= +### Scheduled Service Equipment + +```{eval-rst} +.. autoclass:: wombat.core.data_classes.ScheduledServiceEquipmentData + :members: + :undoc-members: + :exclude-members: name, equipment_rate, day_rate, n_day_rate, hourly_rate, + n_hourly_rate, start_month, start_day, start_year, end_month, end_day, end_year, + capability, mobilization_cost, mobilization_days, speed, max_windspeed_repair, + max_windspeed_transport, max_waveheight_transport, max_waveheight_repair, onsite, + method, max_severity, operating_dates, create_date_range, workday_start, + workday_end, crew, crew_transfer_time, n_crews, strategy, port_distance, + reduced_speed_start, reduced_speed_end, reduced_speed, speed_reduction_factor, + non_operational_end, non_operational_start, +``` + +(types:service-equipment:unscheduled)= +### Unscheduled Service Equipment + +```{eval-rst} +.. autoclass:: wombat.core.data_classes.UnscheduledServiceEquipmentData + :members: + :undoc-members: + :exclude-members: name, equipment_rate, day_rate, n_day_rate, hourly_rate, + n_hourly_rate, start_month, start_day, start_year, end_month, end_day, end_year, + capability, mobilization_cost, mobilization_days, speed, max_windspeed_repair, + max_windspeed_transport, max_waveheight_transport, max_waveheight_repair, onsite, + method, max_severity, operating_dates, create_date_range, workday_start, + workday_end, crew, crew_transfer_time, n_crews, strategy, strategy_threshold, + speed_reduction_factor, non_operational_end, non_operational_start, unmoor_hours, + reconnection_hours, port_distance, reduced_speed_start, reduced_speed_end, + reduced_speed, tow_speed, charter_days +``` + +(types:service-equipment:ports)= +### Port Configuration + +```{eval-rst} +.. autoclass:: wombat.core.data_classes.PortConfig + :members: + :undoc-members: + :exclude-members: name, tugboats, crew, n_crews, max_operations, workday_start, + workday_end, site_distance, annual_fee, non_operational_start, non_operational_end, + reduced_speed_start, reduced_speed_end, reduced_speed, non_operational_dates_set, + reduced_speed_dates_set, non_stop_shift +``` + +## Wind Farm Support + +(types:windfarm:subassembly)= +### Subassembly Model + +```{eval-rst} +.. autoclass:: wombat.core.data_classes.SubassemblyData + :members: + :undoc-members: + :exclude-members: name, maintenance, failures, system_id, system_name, system_value, + subassembly_id, subassembly_name, severity_level, details, cable, upstream_turbines, + rng +``` + +### Wind Farm Map + +```{eval-rst} +.. autoclass:: wombat.core.data_classes.WindFarmMap + :members: + :undoc-members: + :exclude-members: substation_map, export_cables +``` + +### Substation Map + +```{eval-rst} +.. autoclass:: wombat.core.data_classes.SubstationMap + :members: + :undoc-members: + :exclude-members: string_starts, string_map, downstream +``` + +### String + +```{eval-rst} +.. autoclass:: wombat.core.data_classes.String + :members: + :undoc-members: + :exclude-members: start, upstream_map +``` + +### Sub String + +```{eval-rst} +.. autoclass:: wombat.core.data_classes.SubString + :members: + :undoc-members: + :exclude-members: downstream, upstream +``` + +## Miscellaneous + +(types:misc:fixed-costs)= +### Fixed Cost Model + +```{eval-rst} +.. autoclass:: wombat.core.data_classes.FixedCosts + :members: + :undoc-members: + :exclude-members: operations, operations_management_administration, + project_management_administration, marine_management, weather_forecasting, + condition_monitoring, operating_facilities, environmental_health_safety_monitoring, + insurance, brokers_fee, operations_all_risk, business_interruption, + third_party_liability, storm_coverage, annual_leases_fees, submerge_land_lease_costs, + transmission_charges_rights, onshore_electrical_maintenance, labor, resolution, + hierarchy, cost_category_validator +``` diff --git a/_sources/API/utilities.md b/_sources/API/utilities.md new file mode 100644 index 00000000..2fef8efa --- /dev/null +++ b/_sources/API/utilities.md @@ -0,0 +1,33 @@ +# Helpers and Plotting + +## Plotting + +```{eval-rst} +.. automodule:: wombat.utilities.plot + :members: + :undoc-members: +``` + +## Logging functions + +```{eval-rst} +.. automodule:: wombat.utilities.logging + :members: + :undoc-members: +``` + +## Time Calculations + +```{eval-rst} +.. automodule:: wombat.utilities.time + :members: + :undoc-members: +``` + +## Miscellaneous + +```{eval-rst} +.. automodule:: wombat.utilities.utilities + :members: + :undoc-members: +``` diff --git a/_sources/API/windfarm.md b/_sources/API/windfarm.md new file mode 100644 index 00000000..883d0eef --- /dev/null +++ b/_sources/API/windfarm.md @@ -0,0 +1,37 @@ +# Wind Farm Classes + +The wind farm classes define how the wind farm's graph model are created, and power the +timing out of the failures and maintenance events within the simulation for the cables, +substations, and turbines. + +## Wind Farm + +```{eval-rst} +.. automodule:: wombat.windfarm.windfarm + :members: + :undoc-members: +``` + +## System: Wind turbine or substation + +```{eval-rst} +.. automodule:: wombat.windfarm.system.system + :members: + :undoc-members: +``` + +## Subassembly: The modeled components of a system + +```{eval-rst} +.. automodule:: wombat.windfarm.system.subassembly + :members: + :undoc-members: +``` + +## Cable: Hybrid system and subassembly model + +```{eval-rst} +.. automodule:: wombat.windfarm.system.cable + :members: + :undoc-members: +``` diff --git a/_sources/changelog.md b/_sources/changelog.md new file mode 100644 index 00000000..e618ec52 --- /dev/null +++ b/_sources/changelog.md @@ -0,0 +1,4 @@ +# Change Log + +```{include} ../CHANGELOG.md +``` diff --git a/_sources/contributing.md b/_sources/contributing.md new file mode 100644 index 00000000..12ef6995 --- /dev/null +++ b/_sources/contributing.md @@ -0,0 +1,278 @@ +# Contributor's Guide + +(contributing:getting-started)= +## Getting Started + +These contributing guidelines should be read by software developers wishing to contribute code or +documentation changes into WOMBAT, or to push changes upstream to the main WISDEM/WOMBAT repository. + +1. Create a fork of WOMBAT on GitHub +2. Clone your fork of the repository + + ```bash + git clone -b develop https://github.com//WOMBAT.git + ``` + +3. Move into the WOMBAT source code directory + + ```bash + cd WOMBAT/ + ``` + +4. Install WOMBAT in editable mode with the appropriate developer tools + + - ``".[dev]"`` is for the linting, autoformatting, testing, and code checking tools + - ``".[docs]"`` is for the documentation building tools. Ideally, developers should also be + contributing to the documentation, and therefore checking that the documentation builds locally. + + ```bash + pip install -e ".[dev,docs]" + ``` + +5. Turn on the automated linting and code checking tools. Pre-commit runs at the commit level, and + will only check files that have been modified and staged (e.g., `git add `). + + ```bash + pre-commit install + ``` + +## Keeping your fork in sync with WISDEM/WOMBAT + +The "main" WOMBAT repository is regularly updated with ongoing research at NREL and beyond. After +creating and cloning your fork from the previous section, you might be wondering how to keep it +up to date with the latest improvements. + +Please note that the below process may introduce merge conflicts with your work, and this does not +provide guidance about how to deal with those conflicts. Here is a good resource for working on +[merge conflicts](https://www.atlassian.com/git/tutorials/using-branches/merge-conflicts) that will +inevitably arise in development work. + +1. Ensure you're in the WOMBAT folder. This may look different depending on your operating system. + + ```bash + cd /your/path/to/WOMBAT/ + ``` + +2. If you haven't already, add WISDEM/WOMBAT as the "upstream" location (or whichever naming + convention you prefer). + + ```bash + git remote add upstream https://github.com/WISDEM/WOMBAT.git + ``` + + To find the name you've given WISDEM/WOMBAT again, you can simply run the following to display + all the remote sources you're tracking. + + ```bash + git remote -v + ``` + +3. Fetch all the remote changes + + ```bash + git fetch --all + ``` + +4. Sync the upstream (WISDEM) changes + + ```bash + # If there was a new release this will need to be updated + git checkout main + git pull upstream main + + # Most common branch to bring up to speed + git checkout develop + git pull upstream develop + ``` + +5. Bring your feature branch up to date with the latest changes, assuming you started from the + develop branch. + + ```bash + git checkout feature/your_contribution + git merge develop + ``` + +## Issue Tracking + +New feature requests, changes, enhancements, non-methodology features, and bug reports can be filed +as new issues in the [Github.com issue tracker](https://github.com/WISDEM/WOMBAT/issues) at any time. +Please be sure to fully describe the issue. + +For other issues, please email rob.hammond@nrel.gov. + +### Issue Submission Checklist + +1. Does the issue already exist? + Yes: If you find your issue already exists, make relevant comments and add your + [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). + Use a reaction in place of a "+1" comment: + + - 👍 - upvote + - 👎 - downvote + +2. Is this an individual bug report or feature request? +3. Can the bug be easily reproduced? + 1. Be sure to include enough details about your setup and the issue you've encountered + 2. Simplify as much of the code as possible to better isolate the problem +4. Will someone else understand the issue or change requested given the information provided? + +## Repository + +The WOMBAT repository is hosted on Github, and located here: http://github.com/WISDEM/WOMBAT + +This repository is organized using a modified git-flow system. Branches are organized as follows: + +- main: Stable release version. Must have good test coverage and may not have all the newest features. +- develop: Development branch which contains the newest features. Tests must pass, but code may be + unstable. +- feature/xxx: Feature ranch from develop, should reference a GitHub issue number. +- fix/xxx: Bug fix branch from develop, should reference a GitHub issue number. Can be based off + main if this is a necessary patch. + +To work on a feature, please fork WOMBAT first and then create a feature branch in your own fork. +Work out of this feature branch before submitting a pull request. + +Be sure to periodically synchronize the upstream develop branch into your feature branch to avoid +conflicts in the pull request. + +When your branch is ready, make a pull request to WISDEM/WOMBAT through the +[GitHub web interface](https://github.com/WISDEM/WOMBATpulls). + +## Coding Style + +This code uses a ``pre-commit`` workflow where code styling and linting is taken care of when a user +commits their code. Specifically, this code utilizes ``black`` for automatic formatting (line +length, quotation usage, hanging lines, etc.), ``isort`` for automatic import sorting, ``mypy`` +for typing, and ``ruff`` for linting. + +To activate the ``pre-commit`` workflow, the user must install the developer version as outlined in +the [getting started section](contributing:getting-started), and run the following line: + +```bash +pre-commit install +``` + +## Documentation + +Documentation is written primarily using Markdown, with some components written in +ReStructured Text, and is located in the `WOMBAT/docs/` directory. Additionally, all method and class +documentation is written as NumPy-style docstrings in the code itself, with some aspects documented +inline as needed. + +If the `docs` extras haven't already been installed, be sure to do so before you attempt to build +the documentation site. + +```bash +# Navigate to the top level directory of the repository +cd WOMBAT/ + +# Install the additional dependencies +pip install -e ".[docs]" +``` + +Now, run the build command as follows. + +```bash +# Build the docs +jupyter-book build docs +``` + +## Testing + +All code should be paired with a corresponding unit, regression, or integration test written with +the pytest framework. + +To run the tests you can use any of the following commands, depending on your needs. + +1. All the tests and check for test coverage: + + ```bash + pytest --cov=WOMBAT + ``` + +2. All the tests: + + ```bash + pytest + ``` + +(contributing:pull-request)= +## Pull Request + +Pull requests must be made for all changes. Most pull requests should be made against the develop +branch unless patching a bug that needs to be addressed immediately, and only core developers should +make pull requests to the main branch. + +All pull requests, regardless of the base branch, must include updated documentation and pass all +tests. In addition, code coverage should not be significantly negatively affected. + +### Scope + +Encapsulate the changes of one issue, or multiple if they are highly related. Three small pull +requests is greatly preferred over one large pull request. Not only will the review process be +shorter, but the review will be more focused and of higher quality, benefitting the author and code +base. Be sure to write a complete description of these changes in the pull request body. + +### Tests + +All tests must pass. Pull requests will be rejected or have changes requested if tests do not pass, +or cannot pass with changes. Tests are automatically run through Github Actions for any pull request +or push to the main or develop branches, but should also be run locally before submission. + +#### Test Coverage + +The testing framework described below will generate a coverage report from the tests run through +GitHub Actions. Please ensure that your work is fully covered by running them locally with the +coverage report enabled. + +### Documentation + +Include any relevant changes to inline documentation, docstrings, and any of the documentation files +located in `WOMBAT/docs/`. Pull requests will not be accepted until these changes are complete. + +Be sure to build the documentation and run the examples in the CLI locally prior to submission. + +### Changelog + +All changes must be documented appropriately in CHANGELOG.md in the [Unreleased] section. + +## Release Process + +This section is a reference for WOMBAT's maintainers to keep processes largely consistent +over time, regardless of who the core developers are. + +1. Bump version number and metadata in `WOMBAT/__init__.py` +2. Bump version numbers of any dependencies in `setup.py`. Be sure to separate to keep dependencies + separated by what they are required for (see the `project.optional-dependencies` section of + `pyproject.toml`) +3. Update the changelog at `WOMBAT/CHANGELOG.md`, changing the "UNRELEASED" section to the new + version and the release date (e.g. "[2.3 - 2022-01-18]"). +4. Make a pull request into develop with these updates, and be sure to follow the guide in + [Pull Requests](contributing:pull-request). + +5. Merge develop into main through the git command line + + ```bash + git checkout main + git merge develop + git push + ``` + +- Tag the new release version: + + ```bash + git tag -a v1.2.3 -m "Tag message for v1.2.3" + git push origin v1.2.3 + ``` + + The above process will trigger the `.github/workflows/python-publish-test.yml` GitHub Action + that builds the package and pushes it to Test PyPI. If this is successful, you can move to the + next step, otherwise the errors from the action should be addressed, and the tag should be + deleted, then recreated after the fix is published. + +- Deploying a Package to PyPi + - The repository is equipped with a GitHub Action to build and publish new versions to PyPI. A + maintainer can invoke this workflow by creating a new release on GitHub that corresponds to the + created tag in the previous step. + - The action is defined in `.github/workflows/python-publish.yml`. diff --git a/_sources/examples/examples_reference.md b/_sources/examples/examples_reference.md new file mode 100644 index 00000000..57494d18 --- /dev/null +++ b/_sources/examples/examples_reference.md @@ -0,0 +1,58 @@ +# Examples Reference + +This page will provide a brief overview of the varying examples available in the +[examples folder on GitHub](https://github.com/WISDEM/WOMBAT/blob/main/examples). + +## `archival/` + +This contains the old notebooks and data used to prepare the unveiling of the model to +DOE. The results are displayed in +[presentations section](presentations:fy20-doe) of the documentation. + +## Explanations + +The following notebooks are aimed at demonstrating and explaining various functionality +to users. + +### `how_to.ipynb` + +This is a Jupyter Notebook version of the [*How To* section](./how_to.md) of the +documentation that allows users to interact with the walk through. + +### `strategy_demonstration.ipynb` + +This is a Jupyter Notebook version of the +[*Strategy Demonstration* section](./strategy_demonstration.md) of the documentation +that allows users to interact with the walk through. + +### `metrics_demonstration.ipynb` + +This is a Jupyter Notebook version of the +[*Metrics Demonstration* section](./metrics_demonstration.md) of the documentation that +allows users to interact with the varying maintenance strategy models. + +### `NAWEA_interactive_walkthrough.ipynb` + +This is the notebook used for the NAWEA WindTech 2023 Workshop. See +[here](../workshops/nawea_wind_tech_2023.md) for details. + +## Validation + +### `dinwoodie_validation.ipynb` + +This shows the latest results of the model validation using the modeling parameters from +Dinwoodie, et. al, 2015 [^dinwoodie2015reference]. + +### `iea_26_validation.ipynb` + +This shows the latest results of the model validation using the modeling parameters from +IEA, 2016 [^smart2016iea]. + +### `timing_benchmarks.ipynb` + +This notebook is a simple benchmark for comparing run times between releases from NREL's +FY22 & FY23. That said, there has been a change in computers since running this, and +the listed times for v0.7 in the notebooks will not match those in this example. + +[^dinwoodie2015reference]: Iain Dinwoodie, Ole-Erik V Endrerud, Matthias Hofmann, Rebecca Martin, and Iver Bakken Sperstad. Reference cases for verification of operation and maintenance simulation models for offshore wind farms. *Wind Engineering*, 39(1):1–14, 2015. +[^smart2016iea]: Gavin Smart, Aaron Smith, Ethan Warner, Iver Bakken Sperstad, Bob Prinsen, and Roberto Lacal-Arantegui. Iea wind task 26: offshore wind farm baseline documentation. Technical Report, National Renewable Energy Lab.(NREL), Golden, CO (United States), 2016. diff --git a/_sources/examples/how_to.md b/_sources/examples/how_to.md new file mode 100644 index 00000000..c1ce1795 --- /dev/null +++ b/_sources/examples/how_to.md @@ -0,0 +1,651 @@ +--- +jupytext: + text_representation: + extension: .md + format_name: myst +kernelspec: + display_name: Python 3 + language: python + name: python3 +--- + +# How To Use WOMBAT + +This tutorial will walk through the setup, running, and results stages of a WOMBAT +simulation while providing background information about how each component is related. + +## Imports + +The following code block demonstrates a typical setup for working with WOMBAT and +running analyses. + +```{code-cell} ipython3 +from time import perf_counter # timing purposes only + +import numpy as np +import pandas as pd + +from wombat import Simulation +from wombat.core.library import load_yaml, DINWOODIE + +# Seed the random variable for consistently randomized results +np.random.seed(0) + +# Improve the legibility of DataFrames +pd.set_option("display.float_format", '{:,.2f}'.format) +pd.set_option("display.max_rows", 1000) +pd.set_option("display.max_columns", 1000) +``` + +## Defining the Simulation + +The following will demonstrate the required information to run a simulation. For the +purposes of this tutorial, we'll be working with the data under +`library/code_comparison/dinwoodie` in the Github repository, and specifically the base +case. + +One important item to note is that the library structure is enforced within the code so all +data must be placed in the appropriate locations in your analysis' library as follows: + +```{warning} +As of v0.6, the following structure will be adopted to mirror the format of the +[ORBIT library structure](https://github.com/WISDEM/ORBIT/blob/master/ORBIT/core/library.py#L7-L24) +to increase compatibility between similar libraries. + +As of v0.9, the library structure shown below is the only one that will work. +``` + +To help users convert to the new structure, the following method is provided to create +the required folder structure for users. + +```{code-block} python + +from wombat import create_library_structure # located in wombat.core.library + +new_library = "library" +create_library_structure(new_library) +``` + +The above method call will produce the below folder and subfolder structure. + +``` + + ├── project + ├── config <- Project-level configuration files + ├── port <- Port configuration files + ├── plant <- Wind farm layout files + ├── cables <- Export and Array cable configuration files + ├── substations <- Substation configuration files + ├── turbines <- Turbine configuration and power curve files + ├── vessels <- Land-based and offshore servicing equipment configuration files + ├── weather <- Weather profiles + ├── results <- The analysis log files and any saved output data +``` + +As a convenience feature you can import the provided validation data libraries as +`DINWOODIE` or `IEA_26` as is seen in the imports above, and a consistent path will be +enabled. + +In practice, any folder location can be used so long as it follows the subfolder +structure provided here. + +(how_to:configure:layout)= +### Wind Farm Layout + +The wind farm layout is determined by a csv file, `dinwoodie/windfarm/layout.csv` in +this case. Below is a sample of what information is required and how to use each field, +followed by a visual example. It should be noted that none of the headings are +case-sensitive. + +id (required) +: Unique identifier for the asset; no spaces allowed. + +substation_id (required) +: The id field for the substation that the asset connects to; in the case that this is a + substation, then this field should be the same as `id`; no spaces allowed. + +name (required) +: A descriptive name for the turbine, if desired. This can be the same as `id`. + +type (optional) +: One of "turbine" or "substation". This is required to accurately model a + multi-substation wind farm. The base assumption is that a substation connects to + itself as a means to model the export cable connecting to the interconnection point, + however, this is not always the case, as substations may be connected through their + export systems. Using this filed allows for that connection to be modeled accurately. + +longitude (optional) +: The longitudinal position of the asset, can be in any geospatial reference; optional. + +latitude (optional) +: The latitude position of the asset, can be in any geospatial reference; optional. + +string (required) +: The integer, zero-indexed, string number for where the turbine will be positioned. + +order (required) +: The integer, zero-indexed position on the string for where the turbine will be + positioned. + +distance (optional) +: The distance to the upstream asset; if this is calculated (input = 0), then the + straight line distance is calculated using the provided coordinates (WGS-84 assumed). + +subassembly (required) +: The file that defines the asset's modeling parameters. + +upstream_cable (required) +: The file that defines the upstream cable's modeling parameters. + +upstream_cable_name (optional) +: The descriptive name to give to the cable that will be used during logging. This + enables users to use a single cable definition file while maintaining the naming + conventions used for the wind farm being simulated. + +```{note} +In the example below, there are a few noteworthy caveats that will set the stage for +later input reviews: + - The cables are not modeled, which has a couple of implications + - There only needs to be one string "0" + - The cable specifications are required, even if not being modeled (details later) + - longitude, latitude, and distance are all "0" because the spatial locations are not used + - subassembly is all "vestas_v90.yaml", but having to input the turbine subassembly model + means that multiple turbine types can be used on a windfarm. + - This same logic applies to the upstream_cable so that multiple cable types can be + used as appopriate. +``` + +
+ +| id | substation_id | name | type | longitude | latitude | string | order | distance | subassembly | upstream_cable | +| :-- | :-- | :-- | :-- | --: | --: | --: | --: | --: | :-- | :-- | +| OSS1 | OSS1 | OSS1 | substation | 0 | 0 | | | | offshore_substation.yaml | export.yaml | +| S00T1 | OSS1 | S00T1 | turbine | 0 | 0 | 0 | 0 | 0 | vestas_v90.yaml | array.yaml | +| S00T2 | OSS1 | S00T2 | turbine | 0 | 0 | 0 | 1 | 0 | vestas_v90.yaml | array.yaml | +| S00T3 | OSS1 | S00T3 | turbine | 0 | 0 | 0 | 2 | 0 | vestas_v90.yaml | array.yaml | +| ... | +| S00T79 | OSS1 | S00T79 | turbine | 0 | 0 | 0 | 78 | 0 | vestas_v90.yaml | array.yaml | +| S00T80 | OSS1 | S00T80 | turbine | 0 | 0 | 0 | 79 | 0 | vestas_v90.yaml | array.yaml | +
+ +(how_to:configure:weather)= +### Weather Profile + +The weather profile will broadly define the simulation range with its start and stop +points, though a narrower one can be used when defining the simulation (more later). + +The following columns should exist in the data set with the provided guidelines. + +datetime (required) +: A date and time stamp, any format. + +windspeed (required) +: The hourly, mean winds peed, in meters per second at the time stamp. + +waveheight (optional) +: The hourly, mean wave height, in meters, at the time stamp. If waves are not required, + this can be filled with zeros, or be left out entirely. + +Below, is a demonstration of what `weather/alpha_ventus_weather_2002_2014.csv` looks like. + +| datetime | windspeed | waveheight | +| :-- | --: | --: | +| 1/1/02 0:00 | 11.75561096 | 1.281772405 | +| 1/1/02 1:00 | 10.41321252 | 1.586584315 | +| 1/1/02 2:00 | 8.959270788 | 1.725690828 | +| 1/1/02 3:00 | 9.10014808 | 1.680982063 | +| ... | +| 12/31/14 22:00 | 14.40838803 | 0.869625003 | +| 12/31/14 23:00 | 14.04563195 | 0.993031445 | + +(how_to:configure:environment)= +### Environmental Considerations + +In addition to using weather conditions for site characteristics, WOMBAT is able to +model environmental considerations where a port or site cannot be accessed, for example, +when silt builds up and water depths become too low to tow a turbine into the port. +There is also a feature for imposing maximum operating speeds, such as when there are +animal migrations and vessels must slow down to avoid collisions with endangered species. + +Defining the `non_operational_start` and `non_operational_end` at either the servicing +equipment, environment, or port level allows for the creation of an annualized date +range spanning the length of the simulation where operations are not allowed to occur. +When defined at the environment level, all servicing equipment and a port, if defined, +will have this non-operational period applied, and if it's already existing, the more +conservative of the date ranges will be applied. When defined at the port level, all +associated servicing equipment (tugboats) will have the same inuring priority as when +defined at the environment level. + +The same logic applies when defining the `reduced_speed_start` and `reduced_speed_end` +for defining when the operating speeds of servicing equipment are capped at the +`reduced_speed`. As is the case above, these variables can also be defined at the +servicing equipment level for further customization. + +(how_to:configure:fixed-costs)= +### Fixed Costs + +Please see the [`FixedCosts` API documentation](types:misc:fixed-costs) for +details on this optional piece of financial modeling. + +For modeling a tow-to-port strategy that the port rental costs should be included in +this category, and not in the port configuration. + +(how_to:configure:servicing-equipment)= +### Servicing Equipment + +The servicing equipment control the actual repairs within a simulation, and as of v0.5, +there are four different repair strategies that can be used: scheduled, downtime-based +unscheduled, repair-based unscheduled, and tow-to-port. These are options are controlled +through the `capability` settings in each equipment's configuration in conjunction with +the `service_equipment` setting in the maintenance and failure configurations for each +subassembly. + +For complete documentation of how the servicing equipment parameters are defined, please +see the [ServiceEquipmentData API documentation](types:service-equipment) + +Below is a definition of the different equipment codes and their designations to show +the breadth of what can be simulated. These codes do not have separate operating models, +but instead allow the user to specify the types of operations the servicing equipment +will be able to operate on. This model should be aligned with the `service_equipment` +requirements in the subassembly failure and maintenance models. + +RMT +: remote (no actual equipment BUT no special implementation), akin to remote resets + +DRN +: drone, or potentially even helicopters by changing the costs + +CTV +: crew transfer vessel/onsite truck + +SCN +: small crane (i.e., field support vessel or cherry picker) + +LCN +: large crane (i.e., heavy lift vessel or crawler crane) + +CAB +: cabling-specific vessel/vehicle + +DSV +: diving support vessel + +TOW +: tugboat/towing (a failure with this setting will trigger a tow-to-port scenario where +the simulation's `Port` will dispatch the tugboats as needed) + +AHV +: anchor handling vessel (this is a variation on the tugboat for mooring repairs that +will not tow anything between port and site, but operate at the site) + +Aside from the TOW and AHV capabilities there are no operations specific to each +capability. The remaining configurations of a servicing equipment such as equipment +rates, mobilization, labor, and operating limits will define the nature of its operation +in tandem with a failure's `time` field. So for a remote reset (RMT), there will be a +trivial equipment rate, if any, associated with it to account for the specific operations +or resetting the subassembly remotely. Similarly, a drone repair or inspection (DRN) +will not require any onboard crew, so labor will 0, or low if assuming an operator that +is unaccounted for in the site `FixedCosts`, but will require more time than a remote +reset in addition to a higher equipment cost. + +In addition to a variety of servicing equipment types, there is support for +3 different equipment-level dispatch strategies, as described below. For a set of +example scenarios, please see the [strategy demonstration](strategy_demonstration.ipynb). + +scheduled +: dispatch servicing equipment for a specified date range each year + +requests +: dispatch the servicing equipment once a `strategy_threshold` number of requests + that the equipment can service has been reached + +downtime +: dispatch the servicing equipment once the wind farm's operating level reaches the + `strategy_threshold` percent downtime. + +### The System Models + +The actual assets on the windfarm such as cables, turbines, and substations, are +referred to as systems in WOMBAT, and each has their own individual model. Within each +of these systems, there are user-defined subassemblies (or components) that rely on +two types of repair models: + +- maintenance: scheduled, fixed time interval-based maintenance tasks +- failures: unscheduled, Weibull distribution-based modeled, maintenance tasks + +The subassemblies keys in the system YAML definition can be user-defined +to accommodate the varying language among industry groups and generations of wind +technologies. The only restrictions are the YAML keys must not contain any special +characters, and cannot be repeated + +In the example below we show a `generator` subassembly with an annual service task and +frequent manual reset. For a thorough definition, please read the API +documentation of the [Maintenance](types:maintenance:scheduled) and +[Failure](types:maintenance:unscheduled) data classes. Note that the yaml definition below +specifies that maintenance tasks are in a bulleted list format and that failure +defintions require a dictionary-style input with keys to match the severity level of a +given failure. For more details on the complete subassembly definition, please visit the +[Subassembly API documentation](types:windfarm:subassembly). + +```{code-block} yaml +generator: + name: generator # doesn't need to match the subassembly key that becomes System.id + maintenance: + - description: annual service + time: 60 + materials: 18500 + service_equipment: CTV + frequency: 365 + operation_reduction: 0 # default value + failures: + 1: + scale: 0.1333 + shape: 1 + time: 3 + materials: 0 + service_equipment: CTV + operation_reduction: 0.0 + replacement: False # default value + level: 1 # Note that the "level" value matches the key "1" + description: manual reset +``` + +#### Substations + +The substation model relies on two specific inputs, and one subassembly input (transformer). + +capacity_kw +: The capacity of all turbines in the wind farm, neglecting any losses. Only needed if +a $/kw cost basis is being used. + +capex_kw +: The $/kw cost of the machine, if not providing absolute costs. + +Additional keys can be added to represent subassemblies, such as a transformer, in the +same format as the generator example above. Similarly, a user can define as man or as +few of the subassemblies as desired with their preferred naming conventions + +The following is an example of substation YAML definition with no modeled subassemblies. + +```{code-block} yaml +capacity_kw: 670000 +capex_kw: 140 +transformer: + name: transformer + maintenance: + - + description: n/a + time: 0 + materials: 0 + service_equipment: CTV + frequency: 0 + failures: + 1: + scale: 0 + shape: 0 + time: 0 + materials: 0 + service_equipment: [CTV] + operation_reduction: 0 + level: 1 + description: n/a +``` + +#### Turbines + +The turbine has the most to define out of the three systems in the wind farm model. +Similar to the substation, it relies mainly on the subassembly model with a few extra +parameters, as defined here: + +capacity_kw +: The capacity of the system. Only needed if a $/kw cost basis is being used. + +capex_kw +: The $/kw cost of the machine, if not providing absolute costs. + +power_curve: file +: File that provides the power curve definition. + +power_curve: bin_width +The desired interval, in m/s, between adjacent points on the power curve to be used for +power calculations. + +The `windfarm/vestas_v90.yaml` data file provides the following definition in addition +to the maintenance and failure definitions that were shown previously. + +```{code-block} yaml +capacity_kw: 3000 +capex_kw: 1300 +power_curve: + file: vestas_v90_power_curve.csv + bin_width: 0.5 +``` + +The power curve input CSV requires the following two columns: `windspeed_ms` and +`power_kw` that should be defined using the wind speed for a bin, in m/s and the power +produced at that wind speed, in kW. The current method available for generating the +power curve is the IEC 61400-12-1-2 method for a wind-speed binned power curve. If there +is a need/desire for additional power curve methodologies, then +[please submit an issue on the GitHub](https://github.com/WISDEM/WOMBAT/issues)! + +For an open source listing of a variety of land-based, offshore, and distributed wind +turbine power curves, please visit the +[NREL Turbine Models repository](https://github.com/NREL/turbine-models). + +#### Cables + +The array cable is the simplest format in that you only define a descriptive name, +and the maintenance and failure events as below. It should be noted that the scheme +is a combination of both the system and subassembly configuration. + +For export cables that connect substations, as opposed to an interconnection, they will +not create dependencies because it is assumed they bypass the substation altogether, +and connect directly with the export system. This means that if there is a failure at a +downstream substation, the connecting export cable and its upstream turbine, substation, +and cable connections will continue to operate normally. + +```{code-block} yaml +name: array cable +maintenance: + - + description: n/a + time: 0 + materials: 0 + service_equipment: CTV + frequency: 0 +failures: + 1: + scale: 0 + shape: 0 + time: 0 + materials: 0 + operation_reduction: 0 + service_equipment: CAB + level: 1 + description: n/a +``` + +## Set Up the Simulation + +In the remaining sections of the tutorial, we will work towards setting up and running +a simulation. + +### Define the data library path + +The set library enables WOMBAT to easily access and store data files consistently. Here, +the `DINWOODIE` reference is going to be used again. + +```{note} +If a custom library is being used, the `library_path` must be the full path name to the +location of the folder where the configuration data is contained. +``` + +```{warning} +In v0.6, a new library structure +``` + +```{code-cell} ipython3 +library_path = DINWOODIE # or user-defined path for an external data library +``` + +### The configuration file + +In the configuration below, there are a number of data points that will define our +wind farm layout, weather conditions, working hours, customized start and completion +years, project size, financials, and the servicing equipment to be used. Note that there +can be as many or as few of the servicing equipment units as desired. + +The purpose of an overarching configuration file is to provide a single place to define +the primary inputs for a simulation. Below the base configuration is loaded and displayed +with comments to show where each of files are located in the library structure. WOMBAT +will know where to go for these pointers when the simulation is initialized so the data +is constructed and validated correctly. + +```{code-cell} ipython3 +config = load_yaml(library_path / "project/config", "base.yaml") +``` + +```{code-block} yaml +# Contents of: dinwoodie / config / base.yaml +name: dinwoodie_base +weather: alpha_ventus_weather_2002_2014.csv # located in: dinwoodie / weather +service_equipment: +# YAML-encoded list, but could also be created in standard Python list notation with +# square brackets: [ctv1.yaml, ctv2.yaml, ..., hlv_requests.yaml] +# All below equipment configurations are located in: dinwoodie / vessels + - ctv1.yaml + - ctv2.yaml + - ctv3.yaml + - fsv_requests.yaml + - hlv_requests.yaml +layout: layout.csv # located in: dinwoodie / windfarm +inflation_rate: 0 +fixed_costs: fixed_costs.yaml # located in: dinwoodie / project / config +workday_start: 7 +workday_end: 19 +start_year: 2003 +end_year: 2012 +project_capacity: 240 +# port: base_port.yaml <- When running a tow-to-port simulation the port configuration +# pointer is provided here and located in: dinwoodie / project / port +``` + +## Create a simulation + +There are two ways that this could be done, the first is to use the classmethod +`Simulation.from_config()`, which allows for the full path string, a dictionary, or +`Configuration` object to passed as an input, and the second is through a standard +class initialization. + +### Option 1: `Simulation.from_config()` + +Load the file from the `Configuration` object that was created in the prior code black + +```{code-cell} ipython3 + +sim = Simulation.from_config(library_path=library_path, config=config) + +# Delete any files that get initialized through the simulation environment +sim.env.cleanup_log_files() +``` + +### Option 2: `Simulation()` + +Load the configuration file automatically given a library path and configuration file name. + +In this usage, the string "DINWOODIE" can be used because the `Simulation` class knows +to look for this library mapping, as well as the "IEA_26" mapping for the two validation +cases that we demonstrate in the examples folder. + +```{note} +In Option 2, the config parameter can also be set with a dictionary. + +The library path in the configuration file should match the one provided, or the +setup steps will fail in the simulation. +``` + +```{code-cell} ipython3 +sim = Simulation( + library_path="DINWOODIE", # automatically directs to the provided library + config="base.yaml" +) +sim.env.cleanup_log_files() +``` + +### Seeding the simulation random variable + +Using `random_seed` a simulation can be seeded to produce the same results every single +time, or a `random_generator` can be provided to use the same generator for a batch of +identical simulations to better understand the variations in results. + +```{code-cell} ipython3 +sim = Simulation( + library_path="DINWOODIE", # automatically directs to the provided library + config="base.yaml", + random_seed=2023, # integer value indicating how to seed the internally-created generator +) +sim.env.cleanup_log_files() + +rng = np.random.default_rng(seed=2023) # create the generator +sim = Simulation( + library_path="DINWOODIE", # automatically directs to the provided library + config="base.yaml", + random_generator=rng, # generator that can be shared among all processes +) +``` + +## Run the analysis + +When the run method is called, the default run time is for the full length of the +simulation, however, if a shorter run than was previously designed is required for +debugging, or something similar, we can use `sum.run(until=)` to do this. In +the `run` method, not only is the simulation run, but the metrics class is loaded at the +end to quickly transition to results aggregation without any further code. + +```{code-cell} ipython3 +# Timing for a demonstration of performance +start = perf_counter() + +sim.run() + +end = perf_counter() + +timing = end - start +print(f"Run time: {timing / 60:,.2f} minutes") +``` + + +## Metric computation + +For a more complete view of what metrics can be compiled, please see the [metrics notebook](metrics_demonstration.ipynb), though for the sake of demonstration a few methods will +be shown here + +```{code-cell} ipython3 +net_cf = sim.metrics.capacity_factor(which="net", frequency="project", by="windfarm").values[0][0] +gross_cf = sim.metrics.capacity_factor(which="gross", frequency="project", by="windfarm").values[0][0] +print(f" Net Capacity Factor: {net_cf:2.1%}") +print(f"Gross Capacity Factor: {gross_cf:2.1%}") +``` + +```{code-cell} ipython3 +# Report back a subset of the metrics +total = sim.metrics.time_based_availability(frequency="project", by="windfarm") +print(f" Project time-based availability: {total.windfarm[0]:.1%}") + +total = sim.metrics.production_based_availability(frequency="project", by="windfarm") +print(f"Project energy-based availability: {total.windfarm[0]:.1%}") + +total = sim.metrics.equipment_costs(frequency="project", by_equipment=False) +print(f" Project equipment costs: ${total.values[0][0] / sim.metrics.project_capacity:,.2f}/MW") + +``` + +## Optional: Delete the logging files + +In the case that a lot of simulations are going to be run, and the processed outputs +are all that is required, then there is a convenience method to clean up these files +automatically once you are done. + +```{code-cell} ipython3 +sim.env.cleanup_log_files() +``` diff --git a/_sources/examples/index.md b/_sources/examples/index.md new file mode 100644 index 00000000..f1244ce5 --- /dev/null +++ b/_sources/examples/index.md @@ -0,0 +1,135 @@ + +# User Guide + +This page provides an overview of what WOMBAT is currently able to model, broken down by +general category. Following this, there are separate pages for how this is done as +demonstrated through example notebooks. To fully follow the particulars of each example, +it is recommended to see how each model's configuration files are composed. + +For thorough explanations of the design and implementation ethos of the model, please +see our NREL Technical Report: https://www.osti.gov/biblio/1894867, which was published +alongside v0.5.1, so some functionality has been updated. +## Feature Overview + +For a complete and detailed description of the functionality provided in WOMBAT, it is +recommended to read the API documentation. However, that is quite a task, so below is +a short listing of the core functionality that WOMBAT can provide. + +### Post Processing and the Simulation API + +- The [`Simulation` class](simulation-api) and its + [`Configuration`](simulation-api:config) allow users unfamiliar with + Python to run the simulation library with minimal code. This was the primary design + choice for the model: to create a configuration-based model to enable users with nearly + any level Python exposure. +- The [`Metrics`](simulation-api:metrics) class provides a + straightfoward interface for computing a wide variety of operational summary statistics + from performance to wind power plant financials. + +### Environmental Considerations + +- Windspeed (m/s) and wave height (m) for the duration of a simulation (user-provided, + hourly profile) +- Reduced speed periods for animal migrations. This is primarily an offshore-focused + feature, but can be defined at the simulation, port, or servicing equipment level + using the following three variables: `reduced_speed_start`, `reduced_speed_end`, and + `reduced_speed` (km/hr). This translates to a maximum speed of `reduced_speed` being + enforced between the starting and ending dates each year of the simulation. For more + details, see the documentation pages for the + [environment](core:environment), [port](core:port), + [unshcheduled servicing equipment](types:service-equipment:unscheduled), or + [scheduled servicing equipment](types:service-equipment:scheduled). +- Prohibited operation periods. This dictates periods of time where a site or port may + be inaccessible, or when a piece of servicing equipment should not be allowed to be + mobilized/dispatched. Similar to the speed reduction periods, `non_operational_start` + and `non_operational_end` dictate an annual date range for when servicing equipment + can't be active, or a port or the whole site is inaccessible. The same documenation + pages as above can be referenced for more details. + +### System and Subassembly Modeling + +Each substation, turbine, and cable operate with the same core logic of +[maintenance](types:maintenance:scheduled) and [failure](types:maintenance:unscheduled) +events. The following will break down the differences between each of these systems' +modeling assumptions as well as the basic operations of the maintenance and failure models. + +- Maintenance + - `frequency`: based on a fixed number of days between an event + - `operation_reduction`: the percentage of degradation the triggering of this event + cause +- Failure + - Randomly samples from a Weibull distribution to determine the next failure + - `operation_reduction`: the percentage of degradation the triggering of this event + cause + - `replacement`: if `True`, then all preceeding failures and maintenance tasks are + cancelled and the time until the next event is reset because a replacement is + required, otherwise, each successive failure is added to the repair queue +- Commonalities between Substations, Turbines, and Cables + - Maintenance and Failures compose the actual "model" + - `operating_level` provides the real degradation of a model, including if it is turned + off for servicing + - `operating_level_wo_servicing` provides the operting level as if there were no + ongoing operations to know the potential operating operating +- Substations + - Any failure or maintenance with an `operation_reduction` of 1 will turn off every + upstream connected (and modeled) cable and turbine + - For subations that are connected by an export cable this, the upstream connections + made from the export cable will not be considered upstream connections and therefore + shutdown when a downstream substation has a failure +- Array cables + - Similar to substations, any failure or maintenance with an `operation_reduction` of + 1 will turn off every connected upstream cable and turbine on the same string +- Export Cables + - Similar to array cables, any failure or maintenance with an `operation_reduction` of + 1 will turn off every connected upstream substation and string of cable(s) and + turbine(s) + - As noted in the substation section, export cables connecting substations act + independently and not as a string of connected systems + - The final export cable connecting a substation to the interconnection point can + however shut down the entire wind farm + +### Repair Modeling + +- Repair Management: see [here](core:repair-manager) for complete details + - Generally, repairs and maintenance tasks operate on a first-in, first-out basis with + higher severity level `Maintenance.level` and `Failure.level` being addressed first. + - Servicing equipment, however, can specify if they operate on a "severity" or "turbine" + basis that prioritizes focusing on either the highest serverity level first, or + a single system first, respectively. +- Servicing Equipment: see [here](core:service-equipment) for complete details + - Can either be modeled on a scheduled basis, such as a year-round schedule for onsite + equipment or equipment with an annual visit schedule during safe weather, or on + an unscheduled basis with a threshold for number of submitted repair requests or + farm operating level. + - Mobilizations can be modeled with a fixed number of days to get to site and a flat + cost for both scheduled and unscheduled servicing equipment. For scheduled equipment, + the mobilization is scheduled so that the equipment arrives at the site for the + start of it's first scheduled day. + - A wide range of generalized capabilities can be specified for various modeling + scenarios. + - RMT: remote (no actual equipment BUT no special implementation) + - DRN: drone + - CTV: crew transfer vessel/vehicle + - SCN: small crane (i.e., field support vessel) + - LCN: large crane (i.e., heavy lift vessel) + - CAB: cabling vessel/vehicle + - DSV: diving support vessel + - Operating limits can be applied for both transiting and repair logic to ensure site + safety is considered in the model. +- Ports + - Currently only used for tow-to-port repairs or tugboat based repairs, which adds the + following additional capabilities: + - TOW: tugboat or towing equipment + - AHV: anchor handling vessel (tugboat that doesn't trigger tow-to-port) + - See the [API docs](core:port) for more details + +## Examples and Validation Work + +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/WISDEM/WOMBAT/main?filepath=examples) + +Below are a few examples to get started, for users interested in the validation work in +the [code-to-code comparison presentations](presentations:code-comparison), +the notebooks generating [the most up-to-date results can be found in the main repository](https://github.com/WISDEM/WOMBAT/examples/), where there is a separate analysis +for the +[Dinwoodie, et. al, 2015 comparison](https://github.com/WISDEM/WOMBAT/blob/main/examples/dinwoodie_validation.ipynb), +and for the [IEA Task 26, 2016 comparison](https://github.com/WISDEM/blob/main/WOMBAT/examples/iea_26_validation.ipynb). diff --git a/_sources/examples/metrics_demonstration.md b/_sources/examples/metrics_demonstration.md new file mode 100644 index 00000000..64464112 --- /dev/null +++ b/_sources/examples/metrics_demonstration.md @@ -0,0 +1,687 @@ +--- +jupytext: + formats: md:myst + text_representation: + extension: .md + format_name: myst +kernelspec: + display_name: Python 3 + language: python + name: python3 +--- + +(metrics-demo)= +# Demonstration of the Available Metrics + +For a complete list of metrics and their documentation, please see the API Metrics +[documentation](simulation-api:metrics). + +This demonstration will rely on the results produced in the "How To" notebook and serves +as an extension of the API documentation to show what the results will look like +depending on what inputs are provided. + +```{code-cell} ipython3 +:tags: ["output_scroll"] +from pprint import pprint +from functools import partial + +import pandas as pd +from pandas.io.formats.style import Styler + +from wombat.core import Simulation, Metrics + +# Clean up the aesthetics for the pandas outputs +pd.set_option("display.max_rows", 30) +pd.set_option("display.max_columns", 10) +style = partial( + Styler, + table_attributes='style="font-size: 14px; grid-column-count: 6"', + precision=2, + thousands=",", +) +``` + +(metrics-demo:toc)= +## Table of Contents + +Below is a list of top-level sections to demonstrate how to use WOMBAT's `Metrics` +class methods and an explanation of each individual metric. + +If you don't see a metric or result computation that is core to your work, please submit +an [issue](https://github.com/WISDEM/WOMBAT/issues/new) with details on what the metric +is, and how it should be computed. + +- [Setup](metrics-demo:setup): Running a simulation to gather the results +- [Common Parameters](metrics-demo:common-parameters): Explanation of frequently used + parameter settings +- [Availability](metrics-demo:availability): Time-based and energy-based availability +- [Capacity Factor](metrics-demo:cf): Gross and net capacity factor +- [Task Completion Rate](metrics-demo:task-completion): Task completion metrics +- [Equipment Costs](metrics-demo:equipment-costs): Cost breakdowns by servicing equipment +- [Service Equipment Utilization Rate](metrics-demo:utilization-rate): Utilization + of servicing equipment +- [Vessel-Crew Hours at Sea](metrics-demo:vessel-crew-hours): Number of crew or vessel hours + spent at sea +- [Number of Tows](metrics-demo:n-tows): Number of tows breakdowns +- [Labor Costs](metrics-demo:labor-costs): Breakdown of labor costs +- [Equipment and Labor Costs](metrics-demo:equipment-labor-costs): Combined servicing equipment + and labor cost breakdown +- [Emissions](metrics-demo:emissions): Emissions of servicing equipment based on activity +- [Component Costs](metrics-demo:component-costs): Materials costs +- [Fixed Cost Impacts](metrics-demo:fixed-costs): Total fixed costs +- [OpEx](metrics-demo:opex): Project OpEx +- [Process Times](metrics-demo:process-times): Timing of various stages of repair and maintenance +- [Power Production](metrics-demo:power-production): Potential and actually produced power +- [Net Present Value](metrics-demo:npv): Project NPV calculator + +(metrics-demo:setup)= +## Setup + +The simulations from the *How To* notebook are going to be rerun as it is not +recommended to create a Metrics class from scratch due to the large number of inputs +that are required, and the initialization is provided in the simulation API's run method. + +To simplify this process, a feature has been added to save the simulation outputs +required to generate the Metrics inputs and a method to reload those outputs as inputs. + +```{code-cell} ipython3 +:tags: ["output_scroll"] +sim = Simulation("DINWOODIE", "base.yaml") + +# Both of these parameters are True by default for convenience +sim.run(create_metrics=True, save_metrics_inputs=True) + +# Load the metrics data +fpath = sim.env.metrics_input_fname.parent +fname = sim.env.metrics_input_fname.name +metrics = Metrics.from_simulation_outputs(fpath, fname) + +# Delete the log files now that they're loaded in +sim.env.cleanup_log_files() + +# Alternatively, in this case because the simulation was run, we can use the +# following for convenience convenience only +metrics = sim.metrics +``` + +(metrics-demo:common-parameters)= +## Common Parameter Explanations + +Before diving into each and every metric, and how they can be customized, it is worth +noting some of the most common parameters used throughout, and their meanings to reduce +redundancy. The varying output forms are demonstrated in the +[availability](metrics-demo:availability) section below. + +### `frequency` + + project + : Computed across the whole simulation, with the resulting `DataFrame` having an empty + index. + + annual + : Summary of each year in the simulation, with the resulting `DataFrame` having "year" + as the index. + + monthly + : Summary of each month of the year, aggregated across years, with the resulting + `DataFrame` having "month" as the index. + + month-year + : computed on a month-by-year basis, producing the results for every month of the + simulation, with the resulting `DataFrame` having "year" and "month" as the index. + +### `by` + + windfarm + : Aggregated across all turbines, with the resulting `DataFrame` having only + "windfarm" as a column + + turbine + : Computed for each turbine, with the resulting `DataFrame` having a column for each + turbine + +(metrics-demo:availability)= +## Availability + +There are two methods to produce availability, which have their own function calls: + +- energy: actual power produced divided by potential power produced + - {py:meth}`wombat.core.post_processor.Metrics.production_based_availability` +- time: The ratio of all non-zero hours to all hours in the simulation, or the + proportion of the simulation where turbines are operating + - {py:meth}`wombat.core.post_processor.Metrics.time_based_availability` + +Here, we will go through the various input definitions to get time-based availability data as both methods use the same inputs, and provide outputs in the same format. + +**Inputs**: + +- `frequency`, as explained [above](metrics-demo:common-parameters) options: "project", + "annual", "monthly", and "month-year" +- `by`, as explained [above](metrics-demo:common-parameters) options: "windfarm" and + "turbine" + +Below is a demonstration of the variations on `frequency` and `by` for +`time_based_availability`. + +```{code-cell} ipython3 +:tags: ["output_scroll"] +style(metrics.time_based_availability(frequency="project", by="windfarm")) +``` + +```{code-cell} ipython3 +:tags: ["output_scroll"] +style(metrics.production_based_availability(frequency="project", by="windfarm")) +``` + +Note that in the two above examples, that the values are equal. This is due to the fact +that the example simulation does not have any operating reduction applied to failures, +unless it's a catastrophic failure, so there is no expected difference. + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Demonstrate the by turbine granularity +style(metrics.time_based_availability(frequency="project", by="turbine")) +``` + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Demonstrate the annualized outputs +style(metrics.time_based_availability(frequency="annual", by="windfarm")) +``` + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Demonstrate the month aggregations +style(metrics.time_based_availability(frequency="monthly", by="windfarm")) +``` + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Demonstrate the granular monthly reporting +style(metrics.time_based_availability(frequency="month-year", by="windfarm")) +``` + +(metrics-demo:cf)= +## Capacity Factor + +The capacity factor is the ratio of actual (net) or potential (gross) energy production +divided by the project's capacity. For further documentation, see the API docs here: +{py:meth}`wombat.core.post_processor.Metrics.capacity_factor`. + +**Inputs**: + +- `which` + - "net": net capacity factor, actual production divided by the plant capacity + - "gross": gross capacity factor, potential production divided by the plant capacity +- `frequency`, as explained [above](metrics-demo:common-parameters), options: "project", + "annual", "monthly", and "month-year" +- `by`, as explained [above](metrics-demo:common-parameters), options: "windfarm" and + "turbine" + +**Example Usage**: + +```{code-cell} ipython3 +:tags: ["output_scroll"] +net_cf = metrics.capacity_factor(which="net", frequency="project", by="windfarm").values[0][0] +gross_cf = metrics.capacity_factor(which="gross", frequency="project", by="windfarm").values[0][0] +print(f" Net capacity factor: {net_cf:.2%}") +print(f"Gross capacity factor: {gross_cf:.2%}") +``` + +(metrics-demo:task-completion)= +## Task Completion Rate + +The task completion rate is the ratio of tasks completed aggregated to the desired +`frequency`. It is possible to have a >100% completion rate if all maintenance and +failure requests submitted in a time period were completed in addition to those that +went unfinished in prior time periods. For further documentation, see the API docs here: +{py:meth}`wombat.core.post_processor.Metrics.task_completion_rate`. + +**Inputs**: + +- `which` + - "scheduled": scheduled maintenance only (classified as maintenance tasks in inputs) + - "unscheduled": unscheduled maintenance only (classified as failure events in inputs) + - "both": Combined completion rate for all tasks +- `frequency`, as explained [above](metrics-demo:common-parameters), options: "project", + "annual", "monthly", and "month-year" + +**Example Usage**: + +```{code-cell} ipython3 +:tags: ["output_scroll"] +scheduled = metrics.task_completion_rate(which="scheduled", frequency="project").values[0][0] +unscheduled = metrics.task_completion_rate(which="unscheduled", frequency="project").values[0][0] +combined = metrics.task_completion_rate(which="both", frequency="project").values[0][0] +print(f" Scheduled Task Completion Rate: {scheduled:.2%}") +print(f"Unscheduled Task Completion Rate: {unscheduled:.2%}") +print(f" Overall Task Completion Rate: {combined:.2%}") +``` + +(metrics-demo:equipment-costs)= +## Equipment Costs + +Sum of the costs associated with a simulation's servicing equipment, which excludes +materials, downtime, etc. For further documentation, see the API docs here: +{py:meth}`wombat.core.post_processor.Metrics.equipment_costs`. + +**Inputs**: + +- `frequency`, as explained [above](metrics-demo:common-parameters), options: "project", + "annual", "monthly", and "month-year" +- `by_equipment` + - `True`: Aggregates all equipment into a single cost + - `False`: Computes for each unit of servicing equipment + +**Example Usage**: + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Project total at the whole wind farm level +style(metrics.equipment_costs(frequency="project", by_equipment=False)) +``` + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Project totals at servicing equipment level +style(metrics.equipment_costs(frequency="project", by_equipment=True)) +``` + +(metrics-demo:utilization-rate)= +## Service Equipment Utilization Rate + +Ratio of days when the servicing equipment is in use (not delayed for a whole day due to +either weather or lack of repairs to be completed) to the number of days it's present in +the simulation. For further documentation, see the API docs here: +{py:meth}`wombat.core.post_processor.Metrics.service_equipment_utilization`. + +**Inputs**: + +- `frequency`, as explained [above](metrics-demo:common-parameters), options: "project" + and "annual" + +**Example Usage**: + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Project totals +style(metrics.service_equipment_utilization(frequency="project")) +``` + +(metrics-demo:vessel-crew-hours)= +## Vessel-Crew Hours at Sea + +The number of vessel hours or crew hours at sea for offshore wind power plant +simulations. For further documentation, see the API docs here: +{py:meth}`wombat.core.post_processor.Metrics.vessel_crew_hours_at_sea`. + +**Inputs**: + +- `frequency`, as explained [above](metrics-demo:common-parameters), options: "project", + "annual", "monthly", and "month-year" +- `by_equipment` + - `True`: Aggregates all equipment into a single cost + - `False`: Computes for each unit of servicing equipment +- `vessel_crew_assumption`: A dictionary of vessel names + (`ServiceEquipment.settings.name`, but also found at `Metrics.service_equipment_names`) + and the number of crew onboard at any given time. The application of this assumption + transforms the results from vessel hours at sea to crew hours at sea. + +**Example Usage**: + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Project total, not broken out by vessel +style(metrics.vessel_crew_hours_at_sea(frequency="project", by_equipment=False)) +``` + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Annual project totals, broken out by vessel +style(metrics.vessel_crew_hours_at_sea(frequency="annual", by_equipment=True)) +``` + +(metrics-demo:n-tows)= +## Number of Tows + +The number of tows performed during the simulation. If tow-to-port was not used in the +simulation, a DataFrame with a single value of 0 will be returned. For further +documentation, see the API docs here: +{py:meth}`wombat.core.post_processor.Metrics.number_of_tows`. + +**Inputs**: + +- `frequency`, as explained [above](metrics-demo:common-parameters), options: "project", + "annual", "monthly", and "month-year" +- `by_tug` + - `True`: Computed for each tugboat (towing vessel) + - `False`: Aggregates all the tugboats +- `by_direction` + - `True`: Computed for each direction a tow was performed (to port or to site) + - `False`: Aggregates to the total number of tows + +**Example Usage**: + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Project Total +# NOTE: This example has no towing, so it will return 0 +style(metrics.number_of_tows(frequency="project")) +``` + +(metrics-demo:labor-costs)= +## Labor Costs + +Sum of all labor costs associated with servicing equipment, excluding the labor defined +in the fixed costs, which can be broken out by type. For further documentation, see the +API docs here: {py:meth}`wombat.core.post_processor.Metrics.labor_costs`. + +**Inputs**: + +- `frequency`, as explained [above](metrics-demo:common-parameters), options: "project", + "annual", "monthly", and "month-year" +- `by_type` + - `True`: Computed for each labor type (salary and hourly) + - `False`: Aggregates all the labor costs + +**Example Usage**: + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Project total at the whole wind farm level +total = metrics.labor_costs(frequency="project", by_type=False) +print(f"Project total: ${total.values[0][0] / metrics.project_capacity:,.2f}/MW") +``` + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Project totals for each type of labor +# NOTE: this simulation relies on using a fixed labor cost, so this is still $0 +style(metrics.labor_costs(frequency="project", by_type=True)) +``` + +(metrics-demo:equipment-labor-costs)= +## Equipment and Labor Costs + +Sum of all labor and servicing equipment costs, excluding the labor defined in the fixed +costs, which can be broken out by each category. For further documentation, see the API +docs here: {py:meth}`wombat.core.post_processor.Metrics.equipment_labor_cost_breakdown`. + +**Inputs**: + +- `frequency`, as explained [above](metrics-demo:common-parameters), options: "project", + "annual", "monthly", and "month-year" +- `by_category` + - `True`: Computed for each unit servicing equipment and labor category + - `False`: Aggregated to the sum of all costs + +`reason` definitions: + +- Maintenance: routine maintenance, or events defined as a + {py:class}`wombat.core.data_classes.Maintenance` +- Repair: unscheduled maintenance, ranging from inspections to replacements, or events + defined as a {py:class}`wombat.core.data_classes.Failure` +- Mobilization: Cost of mobilizing servicing equipment +- Crew Transfer: Costs incurred while crew are transferring between a turbine or + substation and the servicing equipment +- Site Travel: Costs incurred while transiting to/from the site and while at the site +- Weather Delay: Any delays caused by unsafe weather conditions +- No Requests: Equipment and labor is active, but there are no repairs or maintenance + tasks to be completed +- Not in Shift: Any time outside the operating hours of the wind farm (or the + servicing equipment's specific operating hours) + +**Example Usage**: + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Project totals +style(metrics.equipment_labor_cost_breakdowns(frequency="project", by_category=False)) +``` + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Project totals by each category +style(metrics.equipment_labor_cost_breakdowns(frequency="project", by_category=True)) +``` + +(metrics-demo:emissions)= +## Emissions + +Emissions (tons or other provided units) of all servicing equipment activity, except +overnight waiting periods between shifts. For further documentation, see the API docs +here: {py:meth}`wombat.core.post_processor.Metrics.emissions`. + +**Inputs**: + +- `emissions_factors`: Dictionary of servicing equipment names and the emissions per + hour of the following activities: `transit`, `maneuvering`, `idle at site`, and + `idle at port`, where port is stand-in for wherever the servicing equipment might be + based when not at site. +- `maneuvering_factor`: The proportion of transit time that can generally be associated + with positioning servicing, by default 10%. +- `port_engine_on_factor`: The proportion of the idling at port time when the engine is + running and producing emissions, by default 25%. + +```{code-cell} ipython3 +# Create the emissions factors, in tons per hour +emissions_factors = { + "Crew Transfer Vessel 1": { + "transit": 4, + "maneuvering": 3, + "idle at site": 0.5, + "idle at port": 0.25, + }, + "Crew Transfer Vessel 2": { + "transit": 4, + "maneuvering": 3, + "idle at site": 0.5, + "idle at port": 0.25, + }, + "Crew Transfer Vessel 3": { + "transit": 4, + "maneuvering": 3, + "idle at site": 0.5, + "idle at port": 0.25, + }, + "Field Support Vessel": { + "transit": 6, + "maneuvering": 4, + "idle at site": 1, + "idle at port": 0.5, + }, + "Heavy Lift Vessel": { + "transit": 12, + "maneuvering": 7, + "idle at site": 1, + "idle at port": 0.5, + }, +} + +style(metrics.emissions(emissions_factors=emissions_factors, maneuvering_factor=0.075, port_engine_on_factor=0.20)) +``` + +(metrics-demo:component-costs)= +## Component Costs + +All the costs associated with maintenance and failure events during the simulation, +including delays incurred during the repair process, but excluding costs not directly +tied to a repair. For further documentation, see the API docs here: +{py:meth}`wombat.core.post_processor.Metrics.component_costs`. + +**Inputs**: + +- `frequency`, as explained [above](metrics-demo:common-parameters), options: "project", + "annual", "monthly", and "month-year" +- `by_category` + - `True`: Computed across each cost category + - `False`: Aggregated to the sum of all categories +- `by_action` + - `True`: Computed by each of "repair", "maintenance", and "delay", and is included in + the MultiIndex + - `False`: Aggregated as the sum of all actions + +`action` definitions: + +- maintenance: routine maintenance +- repair: unscheduled maintenance, ranging from inspections to replacements +- delay: Any delays caused by unsafe weather conditions or not being able to finish a + process within a single shift + +**Example Usage**: + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Project totals by component +style(metrics.component_costs(frequency="project", by_category=False, by_action=False)) +``` + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Project totals by each category and action type +style(metrics.component_costs(frequency="project", by_category=True, by_action=True)) +``` + +(metrics-demo:fixed-costs)= +## Fixed Cost Impacts + +Computes the total costs of the fixed costs categories. For further documentation, see +the definition docs, here: {py:class}`wombat.core.data_classes.FixedCosts`, or the API +docs here: {py:meth}`wombat.core.post_processor.Metrics.fixed_costs`. + +**Inputs**: + +- `frequency`, as explained [above](metrics-demo:common-parameters), options: "project", + "annual", "monthly", and "month-year" +- `resolution` (also, demonstrated below) + - "high": Computed across the most granular cost levels + - "medium": Computed for each general cost category + - "low": Aggregated to a single sum of costs + +```{code-cell} ipython3 +:tags: ["output_scroll"] +pprint(metrics.fixed_costs.hierarchy) +``` + +**Example Usage**: + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Project totals at the highest level +# NOTE: there were no fixed costs defined in this example, so all values will be 0, so +# this will just be demonstrating the output format +style(metrics.project_fixed_costs(frequency="project", resolution="low")) +``` + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Project totals at the medium level +style(metrics.project_fixed_costs(frequency="project", resolution="medium")) +``` + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Project totals at the lowest level +style(metrics.project_fixed_costs(frequency="project", resolution="high")) +``` + +(metrics-demo:opex)= +## OpEx + +Computes the total cost of all operating expenditures for the duration of the +simulation, including fixed costs. For further documentation, see the API docs here: +{py:meth}`wombat.core.post_processor.Metrics.opex`. + +**Inputs**: + +- `frequency`, as explained [above](metrics-demo:common-parameters), options: "project", + "annual", "monthly", and "month-year" + +- `by_category` + - `True` shows the port fees, fixed costs, labor costs, equipment costs, and materials + costs in addition the total OpEx + - `False` shows only the total OpEx + +**Example Usage**: + +```{code-cell} ipython3 +:tags: ["output_scroll"] +style(metrics.opex("annual")) +``` + +```{code-cell} ipython3 +:tags: ["output_scroll"] +style(metrics.opex("annual", by_category=True)) +``` + +(metrics-demo:process-times)= +## Process Times + +Computes the total number of hours spent from repair request submission to completion, +performing repairs, and the number of request for each repair category. For further +documentation, see the API docs here: +{py:meth}`wombat.core.post_processor.Metrics.process_times`. + +**Example Usage**: + +```{code-cell} ipython3 +:tags: ["output_scroll"] +style(metrics.process_times()) +``` + +(metrics-demo:power-production)= +## Power Production + +Computes the total power production for the wind farm. For further documentation, see +the API docs here: {py:meth}`wombat.core.post_processor.Metrics.power_production`. + +**Inputs**: + +- `frequency`, as explained [above](metrics-demo:common-parameters), options: "project", + "annual", "monthly", and "month-year" +- `by`, as explained [above](metrics-demo:common-parameters) options: "windfarm" and "turbine" +- `units` + - "kwh": kilowatt-hours (kWh) + - "mwh": megawatt-hours (MWh) + - "gwh": gigawatt-hours (GWh) + +**Example Usage**: + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Project totals, in kWh, at the wind farm level +style(metrics.power_production(frequency="project", by="windfarm", units="kwh")) +``` + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Project totals, in MWh, at the wind farm level +style(metrics.power_production(frequency="project", units="mwh")) +``` + +```{code-cell} ipython3 +:tags: ["output_scroll"] +# Project totals, in GWh, at the wind farm level +style(metrics.power_production(frequency="project")) +``` + +(metrics-demo:npv)= +## Net Present Value + +Calcualtes the net present value (NPV) for the project, as +$NPV = (Power * OfftakePrice - OpEx) / (1 + DiscountRate)$. + +For further documentation, see the API docs here: {py:meth}`wombat.core.post_processor.Metrics.npv`. + +**Inputs**: + +- `frequency`, as explained [above](metrics-demo:common-parameters), options: "project", + "annual", "monthly", and "month-year" +- `discount_rate`: The rate of return that could be earned on alternative investments, + by default 0.025. +- `offtake_price`: Price of energy, per MWh, by default 80. + +```{code-cell} ipython3 +:tags: ["output_scroll"] +style(metrics.opex("annual")) +``` diff --git a/_sources/examples/strategy_demonstration.md b/_sources/examples/strategy_demonstration.md new file mode 100644 index 00000000..9cbdd720 --- /dev/null +++ b/_sources/examples/strategy_demonstration.md @@ -0,0 +1,180 @@ +--- +jupytext: + formats: md:myst + text_representation: + extension: .md + format_name: myst +kernelspec: + display_name: Python 3 + language: python + name: python3 +--- + +# Servicing Strategies + +In this example, we'll demonstrate the essential differences in scheduled servicing, unscheduled servicing, and tow-to-port repair strategies. Each of the examples demonstrated below will be based on the 2015 Dinwoodie, et al. paper, though the variations will be for demonstration purposes only. + +## WOMBAT Setup and Variables + +The vessels that will be changed in this demonstration are the field support vessel (FSV) with capability: "SCN", the heavy lift vessel (HLV) with capability: "LCN", and the tugboats, which have capability: "TOW". + +```{note} +When running tow-to-port a `Port` configuration is also required, which will control the tugboats. However, the port costs will still be accounted for in the +`FixedCosts` class as port fees are assumed to be constant. These costs are not considered in this example, so differences in cost should be taken with a grain +of salt given the reduction of HLV and FSV operational and mobilization costs. +``` + +Scenario descriptions: +- **Scheduled**: exactly the same as the base case (fsv_scheduled.yaml and hlv_scheduled.yaml) +- **Unscheduled: requests**: the FSV and HLV are called to site when 10 requests that + they can service are logged (fsv_requests.yaml and hlv_requests.yaml) +- **Unscheduled: downtime**: the FSV and HLV are called to site once the wind farm's + operating level hits 90% or lower (fsv_downtime.yaml and hlv_downtime.yaml) + - **Unscheduled: tow-to-port**: the FSV and HLV will be replaced with three identical + tugboats (tugboat1.yaml, tugboat2.yaml, tugboat3.yaml), and all the failures associated + with the FSV and HLV will be changed to capability "TOW" to trigger the tow-to-port + repairs. These processes will be triggered on the first request (WOMBAT base + assumption that can't be changed for now). + +In this example, we will demonstrate how the results for the base case for the Dinwoodie, et al. example vary based on how each of the vessels are scheduled. The configuration details all remain the same, regardless of details, except for the strategy information, which is defined as follows: + +This example is set up similarly to that of the validation cases to show how the results differ, and not a step-by-step guide for setting up the analyses. We refer the reader to the extensive [documentation](../API/index.md) and [How To example](how_to.md) for more information. + +## Imports and notebook configuration + +```{code-cell} ipython3 +from copy import deepcopy +from time import perf_counter + +import pandas as pd + +from wombat.core import Simulation +from wombat.core.library import DINWOODIE + +pd.set_option("display.max_rows", 1000) +pd.set_option("display.max_columns", 1000) +pd.options.display.float_format = '{:,.2f}'.format +``` + +## Simulation and results setup + +Here we're providing the names of the configuration files (found at: dinwoodie / config) +without their .yaml extensions (added in later) and the results that we want to compare +between simulations to understand some of the timing and cost trade-offs between +simulations. + +The dictionary of keys and lists will be used to create the results data frame where the +keys will be the indices and the lists will be the row values for each of the above +configurations. + +```{code-cell} ipython3 +configs = [ + "base_scheduled", + "base_requests", + "base_downtime", + "base_tow_to_port", +] + +columns = deepcopy(configs) # Create a unique copy of the config names for column naming +results = { + "availability - time based": [], + "availability - production based": [], + "capacity factor - net": [], + "capacity factor - gross": [], + "power production": [], + "task completion rate": [], + "annual direct O&M cost": [], + "annual vessel cost": [], + "ctv cost": [], + "fsv cost": [], + "hlv cost": [], + "tow cost": [], + "annual repair cost": [], + "annual technician cost": [], + "ctv utilization": [], + "fsv utilization": [], + "hlv utilization": [], + "tow utilization": [], +} +``` + +## Run the simulations and display the results + +```{code-cell} ipython3 +timing_df = pd.DataFrame([], columns=["Load Time (min)", "Run Time (min)"], index=configs) +timing_df.index.name = "Scenario" + +for config in configs: + + # Load the simulation + start = perf_counter() + sim = Simulation(DINWOODIE , f"{config}.yaml") + end = perf_counter() + timing_df.loc[config, "Load Time (min)"] = (end - start) / 60 + + # Run the simulation + start = perf_counter() + sim.run() + end = perf_counter() + timing_df.loc[config, "Run Time (min)"] = (end - start) / 60 + + # Gather the results of interest + years = sim.metrics.events.year.unique().shape[0] + mil = 1000000 + + # Gather the high-level results for the simulation + availability_time = sim.metrics.time_based_availability(frequency="project", by="windfarm") + availability_production = sim.metrics.production_based_availability(frequency="project", by="windfarm") + cf_net = sim.metrics.capacity_factor(which="net", frequency="project", by="windfarm") + cf_gross = sim.metrics.capacity_factor(which="gross", frequency="project", by="windfarm") + power_production = sim.metrics.power_production(frequency="project", by="windfarm") + completion_rate = sim.metrics.task_completion_rate(which="both", frequency="project") + parts = sim.metrics.events[["materials_cost"]].sum().sum() + techs = sim.metrics.project_fixed_costs(frequency="project", resolution="low").operations[0] + total = sim.metrics.events[["total_cost"]].sum().sum() + + # Gather the equipment costs and separate the results by equipment type + equipment = sim.metrics.equipment_costs(frequency="project", by_equipment=True) + equipment_sum = equipment.sum().sum() + hlv = equipment[[el for el in equipment.columns if "Heavy Lift Vessel" in el]].sum().sum() + fsv = equipment[[el for el in equipment.columns if "Field Support Vessel" in el]].sum().sum() + ctv = equipment[[el for el in equipment.columns if "Crew Transfer Vessel" in el]].sum().sum() + tow = equipment[[el for el in equipment.columns if "Tugboat" in el]].sum().sum() + + # Gather the equipment utilization data frame and separate the results by equipment type + utilization = sim.metrics.service_equipment_utilization(frequency="project") + hlv_ur = utilization[[el for el in utilization.columns if "Heavy Lift Vessel" in el]].mean().mean() + fsv_ur = utilization[[el for el in utilization.columns if "Field Support Vessel" in el]].mean().mean() + ctv_ur = utilization[[el for el in utilization.columns if "Crew Transfer Vessel" in el]].mean().mean() + tow_ur = utilization[[el for el in utilization.columns if "Tugboat" in el]].mean().mean() + + # Log the results of interest + results["availability - time based"].append(availability_time.values[0][0]) + results["availability - production based"].append(availability_production.values[0][0]) + results["capacity factor - net"].append(cf_net.values[0][0]) + results["capacity factor - gross"].append(cf_gross.values[0][0]) + results["power production"].append(power_production.values[0][0]) + results["task completion rate"].append(completion_rate.values[0][0]) + results["annual direct O&M cost"].append((total + techs) / mil / years) + results["annual vessel cost"].append(equipment_sum / mil / years) + results["ctv cost"].append(ctv / mil / years) + results["fsv cost"].append(fsv / mil / years) + results["hlv cost"].append(hlv / mil / years) + results["tow cost"].append(tow / mil / years) + results["annual repair cost"].append(parts / mil / years) + results["annual technician cost"].append(techs / mil / years) + results["ctv utilization"].append(ctv_ur) + results["fsv utilization"].append(fsv_ur) + results["hlv utilization"].append(hlv_ur) + results["tow utilization"].append(tow_ur) + + # Clear the logs + sim.env.cleanup_log_files() + +timing_df +``` + +```{code-cell} ipython3 +results_df = pd.DataFrame(results.values(), columns=columns, index=results.keys()).fillna(0) +results_df +``` diff --git a/_sources/index.md b/_sources/index.md new file mode 100644 index 00000000..ba76eeb7 --- /dev/null +++ b/_sources/index.md @@ -0,0 +1,122 @@ +# WOMBAT - Windfarm Operations and Maintenance cost-Benefit Analysis Tool + +[![DOI](https://img.shields.io/badge/DOI-10.2172%2F1894867-brightgreen?link=https://doi.org/10.2172/1894867)](https://www.osti.gov/biblio/1894867) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![PyPI Version](https://badge.fury.io/py/wombat.svg)](https://badge.fury.io/py/wombat) +[![PyPI downloads](https://img.shields.io/pypi/dm/wombat?link=https%3A%2F%2Fpypi.org%2Fproject%2FWOMBAT%2F)](https://pypi.org/project/WOMBAT/) + +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/WISDEM/WOMBAT/main?filepath=examples) +[![Documentation site](https://jupyterbook.org/badge.svg)](https://wisdem.github.io/WOMBAT) + +[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) +[![Black formatter](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![isort import formatter](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) + +## Overview + +The WOMBAT framework is designed to provide an open source tool adhering to FLOSS principles +for the wind farm lifecycle research community. Specifically, WOMBAT is meant to serve as +a what-if, or scenario-based, simulation tool, so that you can model the trade-offs in +decision-making for the operations and maintenance phase of a wind farm. + +As a supplement to this documentation site, there is also an NREL Technical Report that +goes through much of the design and implementation details available at: +https://www.osti.gov/biblio/1894867. If you use this software, please cite it using the +following BibTeX information, or in commonly used citation formats +[here](https://www.osti.gov/biblio/1894867). + +```bibtex + + @techreport{hammond2022wombat, + title = {Windfarm Operations and Maintenance cost-Benefit Analysis Tool (WOMBAT)}, + author = {Hammond, Rob and Cooperman, Aubryn}, + doi = {10.2172/1894867}, + url = {https://www.osti.gov/biblio/1894867}, + place = {United States}, + year = {2022}, + month = {10}, + institution = {National Renewable Energy Lab. (NREL)}, + } +``` + +For any questions, feel free to open up an issue in the repository or email: +rob.hammond@nrel.gov. + +## Latest Changes? + +As of v0.8, a series of bug fixes in the cable, subassembly, repair management, and +servicing equipment models that ensure repairs can't happen twice under limited +circumstances or that more than one repair can occur simultaneously. New features include +an emissions metric and random seeding of simulations, with significant simulation +speedups across the board due to using Polars for managing the weather and datetime +functionality. + +Please see the CHANGELOG for details! + +* On this site: https://wisdem.github.io/WOMBAT/changelog.html +* On GitHub: https://github.com/WISDEM/WOMBAT/blob/main/CHANGELOG.md + +## The Model in 30 Seconds Or Less + +In general, the model has 2 overarching branches: the wind farm itself (the technology +strategy), and the simulation environment (the maintenance strategy). For the wind farm +model we can control the varying assets (system in the code)--substations, turbines, and +cables--as well as the components that comprise each asset (subassemblies in the code). +This separation allows for each turbine, cable, or substation component to have its own +unique failure and maintenance models. + +As for the environment, this is where the discrete event simulation itself happens, in +addition to logging, repair logic, and other necessary modeling pieces. The image +below provides a more visual representation of this description. + +### High Level Architecture + +The code is largely broken up into two categories: the wind farm and objects contained +within it, and the simulation and simulation environment components. The wind farm is +composed of systems: substation(s), cables, and turbines, and each of those systems is +composed of subassemblies (a conglomerate of components). For the simulation environment, +we consider all the pieces that allow the simulation to happen such as the API, +servicing equipment, repair manager to hold and pass on tasks, and results post-processing. + +```{image} images/high_level_diagram.svg +``` + +### Simulation Architecture + +In the diagram below, we demonstrate the lifecycle of the simulation through the +lifecycle of a single failure. + +1) The maximum length of the simulation is defined by the amount of wind and wave + time series data is provided for the simulation. +2) Each subassembly failure model is a random sampling from a Weibull distribution, so + for the sake of clarity we'll consider this to be a catastrophic drivetrain failure. + When the timeout (time to failure) is reached in the simulation, the subassembly's operating level is + reduced to 0%, and a message is passed to the turbine level (the overarching system + model). +3) From there, the turbine will shut off, and signal to all other subassembly models to + not accrue time for their respective maintenance and failure timeouts. Then, the + turbine creates a repair request and passes that to the repair manager. +4) The repair manager will store the submitted task and depending on the servicing + equipment's maintenance strategy, a crane or vessel will be called to site. +5) The vessel (or crane) will mobilize and accrue time there, all the while, the turbine + and wind power plant will accrue downtime. When the servicing equipment arrives at + the site it will operate according to its operation limits, current weather + conditions, and site/equipment-specific working hours and continue to log costs and + downtime. +6) When the servicing is complete, the subassembly will be placed back to good-as-new + condition and the turbine will be reset to operating. From there all the turbine's + and drivetrain's failure and maintenance models be turned back on, and the simulation + will continue on in the same manner until it reaches its user- or weather-defined + ending point. + +```{image} images/simulation_diagram.svg +``` + +## License + +Notice on the NREL application of the Apache-2 license, also found on the +[GitHub](https://github.com/WISDEM/WOMBAT/blob/main/NOTICE), along with the +complete [license](https://github.com/WISDEM/WOMBAT/blob/main/LICENSE) details. + +```{include} ../NOTICE +``` diff --git a/_sources/install.md b/_sources/install.md new file mode 100644 index 00000000..4ddadc7e --- /dev/null +++ b/_sources/install.md @@ -0,0 +1,96 @@ +# Installation + +## Create a conda environment + +Download the latest version of [Miniconda]() +for the appropriate OS. Follow the remaining [steps]() +for the appropriate OS version. + +Using conda, create a new virtual environment, replacing `` with a name +of your choosing (without spaces): +```text +$ conda create -n python=3 +$ conda activate +``` +You can now use ``conda activate `` to enter the environment and +``conda deactivate`` to exit the environment. + + +## Installing + +```{note} +The folowing all asssume you are in your `wombat` environment! +``` + +### Pip + +WOMBAT is listed on PyPI under `wombat`, and can be installed for users that don't +intend to modify the code, by running the following: + +```bash +pip install wombat +``` + +### From Source + +If you plan to read through the code, in addition to running simulations, it is +recommended to clone repository, and install the local version. + +```{note} +It has been noted that some Windows users have had issues with accessing the included +data libraries using the following, so if issues arise, uninstall wombat, and reinstall +using the `pip install -e '.[dev]'` prompt in the next section +``` + +```bash +git clone https://github.com/WISDEM/WOMBAT.git +cd wombat +pip install wombat/ +``` + +### For contributors + +Anyone seeking to work modify the code and contribute to the repository should follow +the below prompt. This repository relies on automatic code formatting and linting +provided through the pre-commit framework, and any contributors should have this +functionality run and pass before submitting pull requests. + +```bash +git clone https://github.com/WISDEM/WOMBAT.git +cd wombat +pip install -e '.[dev]' # some users may need double quotes here, not single quotes +# Required for automatic code formatting! +pre-commit install +``` + +#### Running the tests + +In addition, pytest is used to manage the testing framework, and can be run using the +following to ensure that contributions don't break the existing tests. This will produce +the results of the tests, including code coverage results, if the tests pass. + +```bash +# From the top level of the repository +pytest tests/ +``` + +### For documentation + +Additionally, for users that wish to modify the documentation or build the documentation +site locally, the following prompt with will install the required documentation building +packages. + +```bash +git clone https://github.com/WISDEM/WOMBAT.git +cd wombat +pip install -e '.[docs]' +``` + +#### Build the documentation site + +```bash +jupyter-book build docs +``` + +Now, the documentation site should be able to be viewed locally at +docs/_build/html/index.html diff --git a/_sources/presentations.md b/_sources/presentations.md new file mode 100644 index 00000000..1e32562e --- /dev/null +++ b/_sources/presentations.md @@ -0,0 +1,57 @@ +# Presentations and Publications +## Publications +[![DOI 10.2172/1894867](https://img.shields.io/badge/DOI-10.2172%2F1894867-brightgreen?link=https://doi.org/10.2172/1894867)](https://www.osti.gov/biblio/1894867) + +Please cite our below publication if you use this software in your studies! + +Hammond, Rob, & Cooperman, Aubryn. Windfarm Operations and Maintenance cost-Benefit Analysis Tool (WOMBAT). United States. https://doi.org/10.2172/1894867 + +```bibtex +@techreport{hammond2022wombat, + title = {Windfarm Operations and Maintenance cost-Benefit Analysis Tool (WOMBAT)}, + author = {Hammond, Rob and Cooperman, Aubryn}, + doi = {10.2172/1894867}, + url = {https://www.osti.gov/biblio/1894867}, + place = {United States}, + year = {2022}, + month = {10}, + institution = {National Renewable Energy Lab. (NREL)}, +} +``` + +## 2023 NAWEA WindTech Workshop + +This document is the actual presentation given in associating with the conference +workshop. Please also see the [workshop's homepage](/workshops/nawea_wind_tech_2023). +
+{download}`presentation PDF <../presentation_material/nawea_2023_wombat_tutorial_87913.pdf>` +| [NREL Publications](https://www.nrel.gov/docs/fy24osti/87913.pdf) + +(presentations:fy20-doe)= +## FY20 DOE Presentation + +This document is a link to the Fiscal Year 2020 DOE presentation on the first prototype +of the model. +
+{download}`presentation PDF <../presentation_material/operations_and_maintenance_model_FY20.pdf>` + +(presentations:code-comparison)= +## Code-to-Code Comparison + +This document serves as a demonstration of the capabilities of this work in reference to +two major works to document simulated O&M results from various industry and research +software systems. Here we compare our software to the published results from +Dinwoodie, et. al, 2015 [^dinwoodie2015reference] and IEA, 2016 [^smart2016iea]. +
+{download}`presentation PDF <../presentation_material/code_comparison.pdf>` + + +## IEA Task 26 Presentation + +This presentation was given to the IEA Task Force responsible for [^smart2016iea] +as a demonstration of NREL's efforts on O&M model development. +
+{download}`presentation PDF <../presentation_material/WOMBAT_IEA_task_26_presentation_6_May_2021.pdf>` + +[^dinwoodie2015reference]: Iain Dinwoodie, Ole-Erik V Endrerud, Matthias Hofmann, Rebecca Martin, and Iver Bakken Sperstad. Reference cases for verification of operation and maintenance simulation models for offshore wind farms. *Wind Engineering*, 39(1):1–14, 2015. +[^smart2016iea]: Gavin Smart, Aaron Smith, Ethan Warner, Iver Bakken Sperstad, Bob Prinsen, and Roberto Lacal-Arantegui. Iea wind task 26: offshore wind farm baseline documentation. Technical Report, National Renewable Energy Lab.(NREL), Golden, CO (United States), 2016. diff --git a/_sources/team.md b/_sources/team.md new file mode 100644 index 00000000..ed4f8e51 --- /dev/null +++ b/_sources/team.md @@ -0,0 +1,22 @@ +# Team + +All members are assumed NREL employees, unless otherwise noted. + +## Software + - Rob Hammond + - Alicia Key + +## Methodology + - Rob Hammond + - Aubryn Cooperman + - Aaron Barker + - Annika Eberle + - Matt Shields + - Shawn Sheng + - Owen Roberts + - Alicia Key + +## Leadership + - Rob Hammond + - Annika Eberle + - Matt Shields diff --git a/_sources/workshops/index.md b/_sources/workshops/index.md new file mode 100644 index 00000000..3a7b9f4f --- /dev/null +++ b/_sources/workshops/index.md @@ -0,0 +1,3 @@ +# Workshops + +This is the homepage for all workshops relating to WOMBAT. For now, there is just one, for the 2023 NAWEA WindTech Conference. diff --git a/_sources/workshops/nawea_wind_tech_2023.md b/_sources/workshops/nawea_wind_tech_2023.md new file mode 100644 index 00000000..c11ebbbe --- /dev/null +++ b/_sources/workshops/nawea_wind_tech_2023.md @@ -0,0 +1,58 @@ +# NAWEA WindTech 2023 WOMBAT Tutorial + +## What You'll Need + +- [COREWIND Floating Wind Description](https://corewind.eu/wp-content/uploads/files/publications/COREWIND-D6.1-General-frame-of-the-analysis-and-description-of-the-new-FOW-assessment-app.pdf) +- [COREWIND Floating Wind O&M Strategies Assessment](https://corewind.eu/wp-content/uploads/files/publications/COREWIND-D4.2-Floating-Wind-O-and-M-Strategies-Assessment.pdf) +- Code editor (VSCode is what I use and therefore recommend) +- Working python environment (Miniconda is my preference because it's lightweight) + - [Anaconda](https://docs.anaconda.com/free/anaconda/install) + - [Miniconda](https://docs.conda.io/projects/miniconda/en/latest/miniconda-install.html) +- Basic git proficiency (we just need to clone a project and change branches) +- Basic terminal proficiency (or Windows alternative) +- Basic Python proficiency (WOMBAT requires very little code, but you still need to use Python) + +## Slides and Data + +Just note that the spoken commentary is not included, but the materials to drive that +content are all included in the slides with links to the appropriate documentation pages +and the relevant screenshots so that participants can track down the required data in +the COREWIND publications more easily. + +- The slides can be found [here](https://www.nrel.gov/docs/fy24osti/87913.pdf) +- The accompanying example notebook is + [here](https://github.com/WISDEM/WOMBAT/blob/main/examples/NAWEA_interactive_walkthrough.ipynb). +- Example dataset for the Morro Bay, California, USA 9D layout with connected + substations is [here](https://github.com/WISDEM/WOMBAT/blob/main/library/corewind/). + +## Pre-Workshop Setup + +1. Create a new Python environment (conda instructions) + + ```bash + conda create -n wombat_workshop python=3.10 + conda config --set pip_interop_enabled true + ``` + +2. Download WOMBAT from GitHub + + ```bash + git clone https://github.com/WISDEM/WOMBAT.git + ``` + +3. Install WOMBAT as an editable package + + ```bash + conda activate wombat_workshop # or what you called in the first step + cd wombat/ + pip install -e . + ``` + +4. Ensure that it all worked (assuming no error messages at any of the prior stages) + + ```bash + python + + >>> import wombat + >>> wombat.__version__ + ``` diff --git a/_sphinx_design_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css b/_sphinx_design_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css new file mode 100644 index 00000000..eb19f698 --- /dev/null +++ b/_sphinx_design_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css @@ -0,0 +1 @@ +.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative}details.sd-dropdown .sd-summary-title{font-weight:700;padding-right:3em !important;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary{list-style:none;padding:1em}details.sd-dropdown summary .sd-octicon.no-title{vertical-align:middle}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown summary::-webkit-details-marker{display:none}details.sd-dropdown summary:focus{outline:none}details.sd-dropdown .sd-summary-icon{margin-right:.5em}details.sd-dropdown .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary:hover .sd-summary-up svg,details.sd-dropdown summary:hover .sd-summary-down svg{opacity:1;transform:scale(1.1)}details.sd-dropdown .sd-summary-up svg,details.sd-dropdown .sd-summary-down svg{display:block;opacity:.6}details.sd-dropdown .sd-summary-up,details.sd-dropdown .sd-summary-down{pointer-events:none;position:absolute;right:1em;top:1em}details.sd-dropdown[open]>.sd-summary-title .sd-summary-down{visibility:hidden}details.sd-dropdown:not([open])>.sd-summary-title .sd-summary-up{visibility:hidden}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #0071bc;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0060a0;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem} diff --git a/_sphinx_design_static/design-tabs.js b/_sphinx_design_static/design-tabs.js new file mode 100644 index 00000000..36b38cf0 --- /dev/null +++ b/_sphinx_design_static/design-tabs.js @@ -0,0 +1,27 @@ +var sd_labels_by_text = {}; + +function ready() { + const li = document.getElementsByClassName("sd-tab-label"); + for (const label of li) { + syncId = label.getAttribute("data-sync-id"); + if (syncId) { + label.onclick = onLabelClick; + if (!sd_labels_by_text[syncId]) { + sd_labels_by_text[syncId] = []; + } + sd_labels_by_text[syncId].push(label); + } + } +} + +function onLabelClick() { + // Activate other inputs with the same sync id. + syncId = this.getAttribute("data-sync-id"); + for (label of sd_labels_by_text[syncId]) { + if (label === this) continue; + label.previousElementSibling.checked = true; + } + window.localStorage.setItem("sphinx-design-last-tab", syncId); +} + +document.addEventListener("DOMContentLoaded", ready, false); diff --git a/_static/basic.css b/_static/basic.css new file mode 100644 index 00000000..e760386b --- /dev/null +++ b/_static/basic.css @@ -0,0 +1,925 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 270px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/_static/check-solid.svg b/_static/check-solid.svg new file mode 100644 index 00000000..92fad4b5 --- /dev/null +++ b/_static/check-solid.svg @@ -0,0 +1,4 @@ + + + + diff --git a/_static/clipboard.min.js b/_static/clipboard.min.js new file mode 100644 index 00000000..54b3c463 --- /dev/null +++ b/_static/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.8 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return o}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),c=n.n(e);function a(t){try{return document.execCommand(t)}catch(t){return}}var f=function(t){t=c()(t);return a("cut"),t};var l=function(t){var e,n,o,r=1 + + + + diff --git a/_static/copybutton.css b/_static/copybutton.css new file mode 100644 index 00000000..f1916ec7 --- /dev/null +++ b/_static/copybutton.css @@ -0,0 +1,94 @@ +/* Copy buttons */ +button.copybtn { + position: absolute; + display: flex; + top: .3em; + right: .3em; + width: 1.7em; + height: 1.7em; + opacity: 0; + transition: opacity 0.3s, border .3s, background-color .3s; + user-select: none; + padding: 0; + border: none; + outline: none; + border-radius: 0.4em; + /* The colors that GitHub uses */ + border: #1b1f2426 1px solid; + background-color: #f6f8fa; + color: #57606a; +} + +button.copybtn.success { + border-color: #22863a; + color: #22863a; +} + +button.copybtn svg { + stroke: currentColor; + width: 1.5em; + height: 1.5em; + padding: 0.1em; +} + +div.highlight { + position: relative; +} + +/* Show the copybutton */ +.highlight:hover button.copybtn, button.copybtn.success { + opacity: 1; +} + +.highlight button.copybtn:hover { + background-color: rgb(235, 235, 235); +} + +.highlight button.copybtn:active { + background-color: rgb(187, 187, 187); +} + +/** + * A minimal CSS-only tooltip copied from: + * https://codepen.io/mildrenben/pen/rVBrpK + * + * To use, write HTML like the following: + * + *

Short

+ */ + .o-tooltip--left { + position: relative; + } + + .o-tooltip--left:after { + opacity: 0; + visibility: hidden; + position: absolute; + content: attr(data-tooltip); + padding: .2em; + font-size: .8em; + left: -.2em; + background: grey; + color: white; + white-space: nowrap; + z-index: 2; + border-radius: 2px; + transform: translateX(-102%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); +} + +.o-tooltip--left:hover:after { + display: block; + opacity: 1; + visibility: visible; + transform: translateX(-100%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); + transition-delay: .5s; +} + +/* By default the copy button shouldn't show up when printing a page */ +@media print { + button.copybtn { + display: none; + } +} diff --git a/_static/copybutton.js b/_static/copybutton.js new file mode 100644 index 00000000..2ea7ff3e --- /dev/null +++ b/_static/copybutton.js @@ -0,0 +1,248 @@ +// Localization support +const messages = { + 'en': { + 'copy': 'Copy', + 'copy_to_clipboard': 'Copy to clipboard', + 'copy_success': 'Copied!', + 'copy_failure': 'Failed to copy', + }, + 'es' : { + 'copy': 'Copiar', + 'copy_to_clipboard': 'Copiar al portapapeles', + 'copy_success': '¡Copiado!', + 'copy_failure': 'Error al copiar', + }, + 'de' : { + 'copy': 'Kopieren', + 'copy_to_clipboard': 'In die Zwischenablage kopieren', + 'copy_success': 'Kopiert!', + 'copy_failure': 'Fehler beim Kopieren', + }, + 'fr' : { + 'copy': 'Copier', + 'copy_to_clipboard': 'Copier dans le presse-papier', + 'copy_success': 'Copié !', + 'copy_failure': 'Échec de la copie', + }, + 'ru': { + 'copy': 'Скопировать', + 'copy_to_clipboard': 'Скопировать в буфер', + 'copy_success': 'Скопировано!', + 'copy_failure': 'Не удалось скопировать', + }, + 'zh-CN': { + 'copy': '复制', + 'copy_to_clipboard': '复制到剪贴板', + 'copy_success': '复制成功!', + 'copy_failure': '复制失败', + }, + 'it' : { + 'copy': 'Copiare', + 'copy_to_clipboard': 'Copiato negli appunti', + 'copy_success': 'Copiato!', + 'copy_failure': 'Errore durante la copia', + } +} + +let locale = 'en' +if( document.documentElement.lang !== undefined + && messages[document.documentElement.lang] !== undefined ) { + locale = document.documentElement.lang +} + +let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; +if (doc_url_root == '#') { + doc_url_root = ''; +} + +/** + * SVG files for our copy buttons + */ +let iconCheck = ` + ${messages[locale]['copy_success']} + + +` + +// If the user specified their own SVG use that, otherwise use the default +let iconCopy = ``; +if (!iconCopy) { + iconCopy = ` + ${messages[locale]['copy_to_clipboard']} + + + +` +} + +/** + * Set up copy/paste for code blocks + */ + +const runWhenDOMLoaded = cb => { + if (document.readyState != 'loading') { + cb() + } else if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', cb) + } else { + document.attachEvent('onreadystatechange', function() { + if (document.readyState == 'complete') cb() + }) + } +} + +const codeCellId = index => `codecell${index}` + +// Clears selected text since ClipboardJS will select the text when copying +const clearSelection = () => { + if (window.getSelection) { + window.getSelection().removeAllRanges() + } else if (document.selection) { + document.selection.empty() + } +} + +// Changes tooltip text for a moment, then changes it back +// We want the timeout of our `success` class to be a bit shorter than the +// tooltip and icon change, so that we can hide the icon before changing back. +var timeoutIcon = 2000; +var timeoutSuccessClass = 1500; + +const temporarilyChangeTooltip = (el, oldText, newText) => { + el.setAttribute('data-tooltip', newText) + el.classList.add('success') + // Remove success a little bit sooner than we change the tooltip + // So that we can use CSS to hide the copybutton first + setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) + setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) +} + +// Changes the copy button icon for two seconds, then changes it back +const temporarilyChangeIcon = (el) => { + el.innerHTML = iconCheck; + setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) +} + +const addCopyButtonToCodeCells = () => { + // If ClipboardJS hasn't loaded, wait a bit and try again. This + // happens because we load ClipboardJS asynchronously. + if (window.ClipboardJS === undefined) { + setTimeout(addCopyButtonToCodeCells, 250) + return + } + + // Add copybuttons to all of our code cells + const COPYBUTTON_SELECTOR = 'div.highlight pre'; + const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) + codeCells.forEach((codeCell, index) => { + const id = codeCellId(index) + codeCell.setAttribute('id', id) + + const clipboardButton = id => + `` + codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) + }) + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} + + +var copyTargetText = (trigger) => { + var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); + + // get filtered text + let exclude = '.linenos'; + + let text = filterText(target, exclude); + return formatCopyText(text, '', false, true, true, true, '', '') +} + + // Initialize with a callback so we can modify the text before copy + const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) + + // Update UI with error/success messages + clipboard.on('success', event => { + clearSelection() + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) + temporarilyChangeIcon(event.trigger) + }) + + clipboard.on('error', event => { + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) + }) +} + +runWhenDOMLoaded(addCopyButtonToCodeCells) \ No newline at end of file diff --git a/_static/copybutton_funcs.js b/_static/copybutton_funcs.js new file mode 100644 index 00000000..dbe1aaad --- /dev/null +++ b/_static/copybutton_funcs.js @@ -0,0 +1,73 @@ +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +export function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} diff --git a/_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css b/_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css new file mode 100644 index 00000000..eb19f698 --- /dev/null +++ b/_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css @@ -0,0 +1 @@ +.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative}details.sd-dropdown .sd-summary-title{font-weight:700;padding-right:3em !important;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary{list-style:none;padding:1em}details.sd-dropdown summary .sd-octicon.no-title{vertical-align:middle}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown summary::-webkit-details-marker{display:none}details.sd-dropdown summary:focus{outline:none}details.sd-dropdown .sd-summary-icon{margin-right:.5em}details.sd-dropdown .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary:hover .sd-summary-up svg,details.sd-dropdown summary:hover .sd-summary-down svg{opacity:1;transform:scale(1.1)}details.sd-dropdown .sd-summary-up svg,details.sd-dropdown .sd-summary-down svg{display:block;opacity:.6}details.sd-dropdown .sd-summary-up,details.sd-dropdown .sd-summary-down{pointer-events:none;position:absolute;right:1em;top:1em}details.sd-dropdown[open]>.sd-summary-title .sd-summary-down{visibility:hidden}details.sd-dropdown:not([open])>.sd-summary-title .sd-summary-up{visibility:hidden}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #0071bc;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0060a0;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem} diff --git a/_static/design-tabs.js b/_static/design-tabs.js new file mode 100644 index 00000000..36b38cf0 --- /dev/null +++ b/_static/design-tabs.js @@ -0,0 +1,27 @@ +var sd_labels_by_text = {}; + +function ready() { + const li = document.getElementsByClassName("sd-tab-label"); + for (const label of li) { + syncId = label.getAttribute("data-sync-id"); + if (syncId) { + label.onclick = onLabelClick; + if (!sd_labels_by_text[syncId]) { + sd_labels_by_text[syncId] = []; + } + sd_labels_by_text[syncId].push(label); + } + } +} + +function onLabelClick() { + // Activate other inputs with the same sync id. + syncId = this.getAttribute("data-sync-id"); + for (label of sd_labels_by_text[syncId]) { + if (label === this) continue; + label.previousElementSibling.checked = true; + } + window.localStorage.setItem("sphinx-design-last-tab", syncId); +} + +document.addEventListener("DOMContentLoaded", ready, false); diff --git a/_static/doctools.js b/_static/doctools.js new file mode 100644 index 00000000..d06a71d7 --- /dev/null +++ b/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/_static/documentation_options.js b/_static/documentation_options.js new file mode 100644 index 00000000..22c74677 --- /dev/null +++ b/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '', + LANGUAGE: 'python', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/_static/file.png b/_static/file.png new file mode 100644 index 00000000..a858a410 Binary files /dev/null and b/_static/file.png differ diff --git a/_static/images/logo_binder.svg b/_static/images/logo_binder.svg new file mode 100644 index 00000000..45fecf75 --- /dev/null +++ b/_static/images/logo_binder.svg @@ -0,0 +1,19 @@ + + + + +logo + + + + + + + + diff --git a/_static/images/logo_colab.png b/_static/images/logo_colab.png new file mode 100644 index 00000000..b7560ec2 Binary files /dev/null and b/_static/images/logo_colab.png differ diff --git a/_static/images/logo_deepnote.svg b/_static/images/logo_deepnote.svg new file mode 100644 index 00000000..fa77ebfc --- /dev/null +++ b/_static/images/logo_deepnote.svg @@ -0,0 +1 @@ + diff --git a/_static/images/logo_jupyterhub.svg b/_static/images/logo_jupyterhub.svg new file mode 100644 index 00000000..60cfe9f2 --- /dev/null +++ b/_static/images/logo_jupyterhub.svg @@ -0,0 +1 @@ +logo_jupyterhubHub diff --git a/_static/language_data.js b/_static/language_data.js new file mode 100644 index 00000000..250f5665 --- /dev/null +++ b/_static/language_data.js @@ -0,0 +1,199 @@ +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, is available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/_static/locales/ar/LC_MESSAGES/booktheme.mo b/_static/locales/ar/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..15541a6a Binary files /dev/null and b/_static/locales/ar/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ar/LC_MESSAGES/booktheme.po b/_static/locales/ar/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..edae2ec4 --- /dev/null +++ b/_static/locales/ar/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ar\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "موضوع بواسطة" + +msgid "Open an issue" +msgstr "افتح قضية" + +msgid "Contents" +msgstr "محتويات" + +msgid "Download notebook file" +msgstr "تنزيل ملف دفتر الملاحظات" + +msgid "Sphinx Book Theme" +msgstr "موضوع كتاب أبو الهول" + +msgid "Fullscreen mode" +msgstr "وضع ملء الشاشة" + +msgid "Edit this page" +msgstr "قم بتحرير هذه الصفحة" + +msgid "By" +msgstr "بواسطة" + +msgid "Copyright" +msgstr "حقوق النشر" + +msgid "Source repository" +msgstr "مستودع المصدر" + +msgid "previous page" +msgstr "الصفحة السابقة" + +msgid "next page" +msgstr "الصفحة التالية" + +msgid "Toggle navigation" +msgstr "تبديل التنقل" + +msgid "repository" +msgstr "مخزن" + +msgid "suggest edit" +msgstr "أقترح تحرير" + +msgid "open issue" +msgstr "قضية مفتوحة" + +msgid "Launch" +msgstr "إطلاق" + +msgid "Print to PDF" +msgstr "طباعة إلى PDF" + +msgid "By the" +msgstr "بواسطة" + +msgid "Last updated on" +msgstr "آخر تحديث في" + +msgid "Download source file" +msgstr "تنزيل ملف المصدر" + +msgid "Download this page" +msgstr "قم بتنزيل هذه الصفحة" diff --git a/_static/locales/bg/LC_MESSAGES/booktheme.mo b/_static/locales/bg/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..da951200 Binary files /dev/null and b/_static/locales/bg/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/bg/LC_MESSAGES/booktheme.po b/_static/locales/bg/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..1f363b9d --- /dev/null +++ b/_static/locales/bg/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: bg\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Тема от" + +msgid "Open an issue" +msgstr "Отворете проблем" + +msgid "Contents" +msgstr "Съдържание" + +msgid "Download notebook file" +msgstr "Изтеглете файла на бележника" + +msgid "Sphinx Book Theme" +msgstr "Тема на книгата Sphinx" + +msgid "Fullscreen mode" +msgstr "Режим на цял екран" + +msgid "Edit this page" +msgstr "Редактирайте тази страница" + +msgid "By" +msgstr "От" + +msgid "Copyright" +msgstr "Авторско право" + +msgid "Source repository" +msgstr "Хранилище на източника" + +msgid "previous page" +msgstr "предишна страница" + +msgid "next page" +msgstr "Следваща страница" + +msgid "Toggle navigation" +msgstr "Превключване на навигацията" + +msgid "repository" +msgstr "хранилище" + +msgid "suggest edit" +msgstr "предложи редактиране" + +msgid "open issue" +msgstr "отворен брой" + +msgid "Launch" +msgstr "Стартиране" + +msgid "Print to PDF" +msgstr "Печат в PDF" + +msgid "By the" +msgstr "По" + +msgid "Last updated on" +msgstr "Последна актуализация на" + +msgid "Download source file" +msgstr "Изтеглете изходния файл" + +msgid "Download this page" +msgstr "Изтеглете тази страница" diff --git a/_static/locales/bn/LC_MESSAGES/booktheme.mo b/_static/locales/bn/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..6b96639b Binary files /dev/null and b/_static/locales/bn/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/bn/LC_MESSAGES/booktheme.po b/_static/locales/bn/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..fa543728 --- /dev/null +++ b/_static/locales/bn/LC_MESSAGES/booktheme.po @@ -0,0 +1,63 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: bn\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "থিম দ্বারা" + +msgid "Open an issue" +msgstr "একটি সমস্যা খুলুন" + +msgid "Download notebook file" +msgstr "নোটবুক ফাইল ডাউনলোড করুন" + +msgid "Sphinx Book Theme" +msgstr "স্পিনিক্স বুক থিম" + +msgid "Edit this page" +msgstr "এই পৃষ্ঠাটি সম্পাদনা করুন" + +msgid "By" +msgstr "দ্বারা" + +msgid "Copyright" +msgstr "কপিরাইট" + +msgid "Source repository" +msgstr "উত্স সংগ্রহস্থল" + +msgid "previous page" +msgstr "আগের পৃষ্ঠা" + +msgid "next page" +msgstr "পরবর্তী পৃষ্ঠা" + +msgid "Toggle navigation" +msgstr "নেভিগেশন টগল করুন" + +msgid "open issue" +msgstr "খোলা সমস্যা" + +msgid "Launch" +msgstr "শুরু করা" + +msgid "Print to PDF" +msgstr "পিডিএফ প্রিন্ট করুন" + +msgid "By the" +msgstr "দ্বারা" + +msgid "Last updated on" +msgstr "সর্বশেষ আপডেট" + +msgid "Download source file" +msgstr "উত্স ফাইল ডাউনলোড করুন" + +msgid "Download this page" +msgstr "এই পৃষ্ঠাটি ডাউনলোড করুন" diff --git a/_static/locales/ca/LC_MESSAGES/booktheme.mo b/_static/locales/ca/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..a4dd30e9 Binary files /dev/null and b/_static/locales/ca/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ca/LC_MESSAGES/booktheme.po b/_static/locales/ca/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..22f1569a --- /dev/null +++ b/_static/locales/ca/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ca\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema del" + +msgid "Open an issue" +msgstr "Obriu un número" + +msgid "Download notebook file" +msgstr "Descarregar fitxer de quadern" + +msgid "Sphinx Book Theme" +msgstr "Tema del llibre Esfinx" + +msgid "Edit this page" +msgstr "Editeu aquesta pàgina" + +msgid "By" +msgstr "Per" + +msgid "Copyright" +msgstr "Copyright" + +msgid "Source repository" +msgstr "Dipòsit de fonts" + +msgid "previous page" +msgstr "Pàgina anterior" + +msgid "next page" +msgstr "pàgina següent" + +msgid "Toggle navigation" +msgstr "Commuta la navegació" + +msgid "suggest edit" +msgstr "suggerir edició" + +msgid "open issue" +msgstr "número obert" + +msgid "Launch" +msgstr "Llançament" + +msgid "Print to PDF" +msgstr "Imprimeix a PDF" + +msgid "By the" +msgstr "Per la" + +msgid "Last updated on" +msgstr "Darrera actualització el" + +msgid "Download source file" +msgstr "Baixeu el fitxer font" + +msgid "Download this page" +msgstr "Descarregueu aquesta pàgina" diff --git a/_static/locales/cs/LC_MESSAGES/booktheme.mo b/_static/locales/cs/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..c39e01a6 Binary files /dev/null and b/_static/locales/cs/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/cs/LC_MESSAGES/booktheme.po b/_static/locales/cs/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..afecd9e7 --- /dev/null +++ b/_static/locales/cs/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: cs\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Téma od" + +msgid "Open an issue" +msgstr "Otevřete problém" + +msgid "Contents" +msgstr "Obsah" + +msgid "Download notebook file" +msgstr "Stáhnout soubor poznámkového bloku" + +msgid "Sphinx Book Theme" +msgstr "Téma knihy Sfinga" + +msgid "Fullscreen mode" +msgstr "Režim celé obrazovky" + +msgid "Edit this page" +msgstr "Upravit tuto stránku" + +msgid "By" +msgstr "Podle" + +msgid "Copyright" +msgstr "autorská práva" + +msgid "Source repository" +msgstr "Zdrojové úložiště" + +msgid "previous page" +msgstr "předchozí stránka" + +msgid "next page" +msgstr "další strana" + +msgid "Toggle navigation" +msgstr "Přepnout navigaci" + +msgid "repository" +msgstr "úložiště" + +msgid "suggest edit" +msgstr "navrhnout úpravy" + +msgid "open issue" +msgstr "otevřené číslo" + +msgid "Launch" +msgstr "Zahájení" + +msgid "Print to PDF" +msgstr "Tisk do PDF" + +msgid "By the" +msgstr "Podle" + +msgid "Last updated on" +msgstr "Naposledy aktualizováno" + +msgid "Download source file" +msgstr "Stáhněte si zdrojový soubor" + +msgid "Download this page" +msgstr "Stáhněte si tuto stránku" diff --git a/_static/locales/da/LC_MESSAGES/booktheme.mo b/_static/locales/da/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..f43157d7 Binary files /dev/null and b/_static/locales/da/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/da/LC_MESSAGES/booktheme.po b/_static/locales/da/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..649c78a8 --- /dev/null +++ b/_static/locales/da/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: da\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema af" + +msgid "Open an issue" +msgstr "Åbn et problem" + +msgid "Contents" +msgstr "Indhold" + +msgid "Download notebook file" +msgstr "Download notesbog-fil" + +msgid "Sphinx Book Theme" +msgstr "Sphinx bogtema" + +msgid "Fullscreen mode" +msgstr "Fuldskærmstilstand" + +msgid "Edit this page" +msgstr "Rediger denne side" + +msgid "By" +msgstr "Ved" + +msgid "Copyright" +msgstr "ophavsret" + +msgid "Source repository" +msgstr "Kildelager" + +msgid "previous page" +msgstr "forrige side" + +msgid "next page" +msgstr "Næste side" + +msgid "Toggle navigation" +msgstr "Skift navigation" + +msgid "repository" +msgstr "lager" + +msgid "suggest edit" +msgstr "foreslå redigering" + +msgid "open issue" +msgstr "åbent nummer" + +msgid "Launch" +msgstr "Start" + +msgid "Print to PDF" +msgstr "Udskriv til PDF" + +msgid "By the" +msgstr "Ved" + +msgid "Last updated on" +msgstr "Sidst opdateret den" + +msgid "Download source file" +msgstr "Download kildefil" + +msgid "Download this page" +msgstr "Download denne side" diff --git a/_static/locales/de/LC_MESSAGES/booktheme.mo b/_static/locales/de/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..648b565c Binary files /dev/null and b/_static/locales/de/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/de/LC_MESSAGES/booktheme.po b/_static/locales/de/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..f51d2ecc --- /dev/null +++ b/_static/locales/de/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Thema von der" + +msgid "Open an issue" +msgstr "Öffnen Sie ein Problem" + +msgid "Contents" +msgstr "Inhalt" + +msgid "Download notebook file" +msgstr "Notebook-Datei herunterladen" + +msgid "Sphinx Book Theme" +msgstr "Sphinx-Buch-Thema" + +msgid "Fullscreen mode" +msgstr "Vollbildmodus" + +msgid "Edit this page" +msgstr "Bearbeite diese Seite" + +msgid "By" +msgstr "Durch" + +msgid "Copyright" +msgstr "Urheberrechte ©" + +msgid "Source repository" +msgstr "Quell-Repository" + +msgid "previous page" +msgstr "vorherige Seite" + +msgid "next page" +msgstr "Nächste Seite" + +msgid "Toggle navigation" +msgstr "Navigation umschalten" + +msgid "repository" +msgstr "Repository" + +msgid "suggest edit" +msgstr "vorschlagen zu bearbeiten" + +msgid "open issue" +msgstr "offenes Thema" + +msgid "Launch" +msgstr "Starten" + +msgid "Print to PDF" +msgstr "In PDF drucken" + +msgid "By the" +msgstr "Bis zum" + +msgid "Last updated on" +msgstr "Zuletzt aktualisiert am" + +msgid "Download source file" +msgstr "Quelldatei herunterladen" + +msgid "Download this page" +msgstr "Laden Sie diese Seite herunter" diff --git a/_static/locales/el/LC_MESSAGES/booktheme.mo b/_static/locales/el/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..fca6e935 Binary files /dev/null and b/_static/locales/el/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/el/LC_MESSAGES/booktheme.po b/_static/locales/el/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..8bec7905 --- /dev/null +++ b/_static/locales/el/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: el\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Θέμα από το" + +msgid "Open an issue" +msgstr "Ανοίξτε ένα ζήτημα" + +msgid "Contents" +msgstr "Περιεχόμενα" + +msgid "Download notebook file" +msgstr "Λήψη αρχείου σημειωματάριου" + +msgid "Sphinx Book Theme" +msgstr "Θέμα βιβλίου Sphinx" + +msgid "Fullscreen mode" +msgstr "ΛΕΙΤΟΥΡΓΙΑ ΠΛΗΡΟΥΣ ΟΘΟΝΗΣ" + +msgid "Edit this page" +msgstr "Επεξεργαστείτε αυτήν τη σελίδα" + +msgid "By" +msgstr "Με" + +msgid "Copyright" +msgstr "Πνευματική ιδιοκτησία" + +msgid "Source repository" +msgstr "Αποθήκη πηγής" + +msgid "previous page" +msgstr "προηγούμενη σελίδα" + +msgid "next page" +msgstr "επόμενη σελίδα" + +msgid "Toggle navigation" +msgstr "Εναλλαγή πλοήγησης" + +msgid "repository" +msgstr "αποθήκη" + +msgid "suggest edit" +msgstr "προτείνω επεξεργασία" + +msgid "open issue" +msgstr "ανοιχτό ζήτημα" + +msgid "Launch" +msgstr "Εκτόξευση" + +msgid "Print to PDF" +msgstr "Εκτύπωση σε PDF" + +msgid "By the" +msgstr "Από το" + +msgid "Last updated on" +msgstr "Τελευταία ενημέρωση στις" + +msgid "Download source file" +msgstr "Λήψη αρχείου προέλευσης" + +msgid "Download this page" +msgstr "Λήψη αυτής της σελίδας" diff --git a/_static/locales/eo/LC_MESSAGES/booktheme.mo b/_static/locales/eo/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..d1072bbe Binary files /dev/null and b/_static/locales/eo/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/eo/LC_MESSAGES/booktheme.po b/_static/locales/eo/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..d72a0481 --- /dev/null +++ b/_static/locales/eo/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: eo\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Temo de la" + +msgid "Open an issue" +msgstr "Malfermu numeron" + +msgid "Contents" +msgstr "Enhavo" + +msgid "Download notebook file" +msgstr "Elŝutu kajeran dosieron" + +msgid "Sphinx Book Theme" +msgstr "Sfinksa Libro-Temo" + +msgid "Fullscreen mode" +msgstr "Plenekrana reĝimo" + +msgid "Edit this page" +msgstr "Redaktu ĉi tiun paĝon" + +msgid "By" +msgstr "De" + +msgid "Copyright" +msgstr "Kopirajto" + +msgid "Source repository" +msgstr "Fonto-deponejo" + +msgid "previous page" +msgstr "antaŭa paĝo" + +msgid "next page" +msgstr "sekva paĝo" + +msgid "Toggle navigation" +msgstr "Ŝalti navigadon" + +msgid "repository" +msgstr "deponejo" + +msgid "suggest edit" +msgstr "sugesti redaktadon" + +msgid "open issue" +msgstr "malferma numero" + +msgid "Launch" +msgstr "Lanĉo" + +msgid "Print to PDF" +msgstr "Presi al PDF" + +msgid "By the" +msgstr "Per la" + +msgid "Last updated on" +msgstr "Laste ĝisdatigita la" + +msgid "Download source file" +msgstr "Elŝutu fontodosieron" + +msgid "Download this page" +msgstr "Elŝutu ĉi tiun paĝon" diff --git a/_static/locales/es/LC_MESSAGES/booktheme.mo b/_static/locales/es/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..ba2ee4dc Binary files /dev/null and b/_static/locales/es/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/es/LC_MESSAGES/booktheme.po b/_static/locales/es/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..611834b2 --- /dev/null +++ b/_static/locales/es/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema por el" + +msgid "Open an issue" +msgstr "Abrir un problema" + +msgid "Contents" +msgstr "Contenido" + +msgid "Download notebook file" +msgstr "Descargar archivo de cuaderno" + +msgid "Sphinx Book Theme" +msgstr "Tema del libro de la esfinge" + +msgid "Fullscreen mode" +msgstr "Modo de pantalla completa" + +msgid "Edit this page" +msgstr "Edita esta página" + +msgid "By" +msgstr "Por" + +msgid "Copyright" +msgstr "Derechos de autor" + +msgid "Source repository" +msgstr "Repositorio de origen" + +msgid "previous page" +msgstr "pagina anterior" + +msgid "next page" +msgstr "siguiente página" + +msgid "Toggle navigation" +msgstr "Navegación de palanca" + +msgid "repository" +msgstr "repositorio" + +msgid "suggest edit" +msgstr "sugerir editar" + +msgid "open issue" +msgstr "Tema abierto" + +msgid "Launch" +msgstr "Lanzamiento" + +msgid "Print to PDF" +msgstr "Imprimir en PDF" + +msgid "By the" +msgstr "Por el" + +msgid "Last updated on" +msgstr "Ultima actualización en" + +msgid "Download source file" +msgstr "Descargar archivo fuente" + +msgid "Download this page" +msgstr "Descarga esta pagina" diff --git a/_static/locales/et/LC_MESSAGES/booktheme.mo b/_static/locales/et/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..983b8239 Binary files /dev/null and b/_static/locales/et/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/et/LC_MESSAGES/booktheme.po b/_static/locales/et/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..345088f0 --- /dev/null +++ b/_static/locales/et/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: et\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Teema" + +msgid "Open an issue" +msgstr "Avage probleem" + +msgid "Contents" +msgstr "Sisu" + +msgid "Download notebook file" +msgstr "Laadige sülearvuti fail alla" + +msgid "Sphinx Book Theme" +msgstr "Sfinksiraamatu teema" + +msgid "Fullscreen mode" +msgstr "Täisekraanirežiim" + +msgid "Edit this page" +msgstr "Muutke seda lehte" + +msgid "By" +msgstr "Kõrval" + +msgid "Copyright" +msgstr "Autoriõigus" + +msgid "Source repository" +msgstr "Allikahoidla" + +msgid "previous page" +msgstr "eelmine leht" + +msgid "next page" +msgstr "järgmine leht" + +msgid "Toggle navigation" +msgstr "Lülita navigeerimine sisse" + +msgid "repository" +msgstr "hoidla" + +msgid "suggest edit" +msgstr "soovita muuta" + +msgid "open issue" +msgstr "avatud küsimus" + +msgid "Launch" +msgstr "Käivitage" + +msgid "Print to PDF" +msgstr "Prindi PDF-i" + +msgid "By the" +msgstr "Autor" + +msgid "Last updated on" +msgstr "Viimati uuendatud" + +msgid "Download source file" +msgstr "Laadige alla lähtefail" + +msgid "Download this page" +msgstr "Laadige see leht alla" diff --git a/_static/locales/fi/LC_MESSAGES/booktheme.mo b/_static/locales/fi/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..d8ac0545 Binary files /dev/null and b/_static/locales/fi/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/fi/LC_MESSAGES/booktheme.po b/_static/locales/fi/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..d97a08dc --- /dev/null +++ b/_static/locales/fi/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fi\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Teeman tekijä" + +msgid "Open an issue" +msgstr "Avaa ongelma" + +msgid "Contents" +msgstr "Sisällys" + +msgid "Download notebook file" +msgstr "Lataa muistikirjatiedosto" + +msgid "Sphinx Book Theme" +msgstr "Sphinx-kirjan teema" + +msgid "Fullscreen mode" +msgstr "Koko näytön tila" + +msgid "Edit this page" +msgstr "Muokkaa tätä sivua" + +msgid "By" +msgstr "Tekijä" + +msgid "Copyright" +msgstr "Tekijänoikeus" + +msgid "Source repository" +msgstr "Lähteen arkisto" + +msgid "previous page" +msgstr "Edellinen sivu" + +msgid "next page" +msgstr "seuraava sivu" + +msgid "Toggle navigation" +msgstr "Vaihda navigointia" + +msgid "repository" +msgstr "arkisto" + +msgid "suggest edit" +msgstr "ehdottaa muokkausta" + +msgid "open issue" +msgstr "avoin ongelma" + +msgid "Launch" +msgstr "Tuoda markkinoille" + +msgid "Print to PDF" +msgstr "Tulosta PDF-tiedostoon" + +msgid "By the" +msgstr "Mukaan" + +msgid "Last updated on" +msgstr "Viimeksi päivitetty" + +msgid "Download source file" +msgstr "Lataa lähdetiedosto" + +msgid "Download this page" +msgstr "Lataa tämä sivu" diff --git a/_static/locales/fr/LC_MESSAGES/booktheme.mo b/_static/locales/fr/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..f663d39f Binary files /dev/null and b/_static/locales/fr/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/fr/LC_MESSAGES/booktheme.po b/_static/locales/fr/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..88f35173 --- /dev/null +++ b/_static/locales/fr/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Thème par le" + +msgid "Open an issue" +msgstr "Ouvrez un problème" + +msgid "Contents" +msgstr "Contenu" + +msgid "Download notebook file" +msgstr "Télécharger le fichier notebook" + +msgid "Sphinx Book Theme" +msgstr "Thème du livre Sphinx" + +msgid "Fullscreen mode" +msgstr "Mode plein écran" + +msgid "Edit this page" +msgstr "Modifier cette page" + +msgid "By" +msgstr "Par" + +msgid "Copyright" +msgstr "droits d'auteur" + +msgid "Source repository" +msgstr "Dépôt source" + +msgid "previous page" +msgstr "page précédente" + +msgid "next page" +msgstr "page suivante" + +msgid "Toggle navigation" +msgstr "Basculer la navigation" + +msgid "repository" +msgstr "dépôt" + +msgid "suggest edit" +msgstr "suggestion de modification" + +msgid "open issue" +msgstr "signaler un problème" + +msgid "Launch" +msgstr "lancement" + +msgid "Print to PDF" +msgstr "Imprimer au format PDF" + +msgid "By the" +msgstr "Par le" + +msgid "Last updated on" +msgstr "Dernière mise à jour le" + +msgid "Download source file" +msgstr "Télécharger le fichier source" + +msgid "Download this page" +msgstr "Téléchargez cette page" diff --git a/_static/locales/hr/LC_MESSAGES/booktheme.mo b/_static/locales/hr/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..eca4a1a2 Binary files /dev/null and b/_static/locales/hr/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/hr/LC_MESSAGES/booktheme.po b/_static/locales/hr/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..fb9440ac --- /dev/null +++ b/_static/locales/hr/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: hr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema autora" + +msgid "Open an issue" +msgstr "Otvorite izdanje" + +msgid "Contents" +msgstr "Sadržaj" + +msgid "Download notebook file" +msgstr "Preuzmi datoteku bilježnice" + +msgid "Sphinx Book Theme" +msgstr "Tema knjige Sphinx" + +msgid "Fullscreen mode" +msgstr "Način preko cijelog zaslona" + +msgid "Edit this page" +msgstr "Uredite ovu stranicu" + +msgid "By" +msgstr "Po" + +msgid "Copyright" +msgstr "Autorska prava" + +msgid "Source repository" +msgstr "Izvorno spremište" + +msgid "previous page" +msgstr "Prethodna stranica" + +msgid "next page" +msgstr "sljedeća stranica" + +msgid "Toggle navigation" +msgstr "Uključi / isključi navigaciju" + +msgid "repository" +msgstr "spremište" + +msgid "suggest edit" +msgstr "predloži uređivanje" + +msgid "open issue" +msgstr "otvoreno izdanje" + +msgid "Launch" +msgstr "Pokrenite" + +msgid "Print to PDF" +msgstr "Ispis u PDF" + +msgid "By the" +msgstr "Od strane" + +msgid "Last updated on" +msgstr "Posljednje ažuriranje:" + +msgid "Download source file" +msgstr "Preuzmi izvornu datoteku" + +msgid "Download this page" +msgstr "Preuzmite ovu stranicu" diff --git a/_static/locales/id/LC_MESSAGES/booktheme.mo b/_static/locales/id/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..d07a06a9 Binary files /dev/null and b/_static/locales/id/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/id/LC_MESSAGES/booktheme.po b/_static/locales/id/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..9ffb56f7 --- /dev/null +++ b/_static/locales/id/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: id\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema oleh" + +msgid "Open an issue" +msgstr "Buka masalah" + +msgid "Contents" +msgstr "Isi" + +msgid "Download notebook file" +msgstr "Unduh file notebook" + +msgid "Sphinx Book Theme" +msgstr "Tema Buku Sphinx" + +msgid "Fullscreen mode" +msgstr "Mode layar penuh" + +msgid "Edit this page" +msgstr "Edit halaman ini" + +msgid "By" +msgstr "Oleh" + +msgid "Copyright" +msgstr "hak cipta" + +msgid "Source repository" +msgstr "Repositori sumber" + +msgid "previous page" +msgstr "halaman sebelumnya" + +msgid "next page" +msgstr "halaman selanjutnya" + +msgid "Toggle navigation" +msgstr "Alihkan navigasi" + +msgid "repository" +msgstr "gudang" + +msgid "suggest edit" +msgstr "menyarankan edit" + +msgid "open issue" +msgstr "masalah terbuka" + +msgid "Launch" +msgstr "Meluncurkan" + +msgid "Print to PDF" +msgstr "Cetak ke PDF" + +msgid "By the" +msgstr "Oleh" + +msgid "Last updated on" +msgstr "Terakhir diperbarui saat" + +msgid "Download source file" +msgstr "Unduh file sumber" + +msgid "Download this page" +msgstr "Unduh halaman ini" diff --git a/_static/locales/it/LC_MESSAGES/booktheme.mo b/_static/locales/it/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..53ba476e Binary files /dev/null and b/_static/locales/it/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/it/LC_MESSAGES/booktheme.po b/_static/locales/it/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..04308dd2 --- /dev/null +++ b/_static/locales/it/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema di" + +msgid "Open an issue" +msgstr "Apri un problema" + +msgid "Contents" +msgstr "Contenuti" + +msgid "Download notebook file" +msgstr "Scarica il file del taccuino" + +msgid "Sphinx Book Theme" +msgstr "Tema del libro della Sfinge" + +msgid "Fullscreen mode" +msgstr "Modalità schermo intero" + +msgid "Edit this page" +msgstr "Modifica questa pagina" + +msgid "By" +msgstr "Di" + +msgid "Copyright" +msgstr "Diritto d'autore" + +msgid "Source repository" +msgstr "Repository di origine" + +msgid "previous page" +msgstr "pagina precedente" + +msgid "next page" +msgstr "pagina successiva" + +msgid "Toggle navigation" +msgstr "Attiva / disattiva la navigazione" + +msgid "repository" +msgstr "repository" + +msgid "suggest edit" +msgstr "suggerisci modifica" + +msgid "open issue" +msgstr "questione aperta" + +msgid "Launch" +msgstr "Lanciare" + +msgid "Print to PDF" +msgstr "Stampa in PDF" + +msgid "By the" +msgstr "Dal" + +msgid "Last updated on" +msgstr "Ultimo aggiornamento il" + +msgid "Download source file" +msgstr "Scarica il file sorgente" + +msgid "Download this page" +msgstr "Scarica questa pagina" diff --git a/_static/locales/iw/LC_MESSAGES/booktheme.mo b/_static/locales/iw/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..a45c6575 Binary files /dev/null and b/_static/locales/iw/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/iw/LC_MESSAGES/booktheme.po b/_static/locales/iw/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..4ea190d3 --- /dev/null +++ b/_static/locales/iw/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: iw\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "נושא מאת" + +msgid "Open an issue" +msgstr "פתח גיליון" + +msgid "Contents" +msgstr "תוכן" + +msgid "Download notebook file" +msgstr "הורד קובץ מחברת" + +msgid "Sphinx Book Theme" +msgstr "נושא ספר ספינקס" + +msgid "Fullscreen mode" +msgstr "מצב מסך מלא" + +msgid "Edit this page" +msgstr "ערוך דף זה" + +msgid "By" +msgstr "על ידי" + +msgid "Copyright" +msgstr "זכויות יוצרים" + +msgid "Source repository" +msgstr "מאגר המקורות" + +msgid "previous page" +msgstr "עמוד קודם" + +msgid "next page" +msgstr "עמוד הבא" + +msgid "Toggle navigation" +msgstr "החלף ניווט" + +msgid "repository" +msgstr "מאגר" + +msgid "suggest edit" +msgstr "מציע לערוך" + +msgid "open issue" +msgstr "בעיה פתוחה" + +msgid "Launch" +msgstr "לְהַשִׁיק" + +msgid "Print to PDF" +msgstr "הדפס לקובץ PDF" + +msgid "By the" +msgstr "דרך" + +msgid "Last updated on" +msgstr "עודכן לאחרונה ב" + +msgid "Download source file" +msgstr "הורד את קובץ המקור" + +msgid "Download this page" +msgstr "הורד דף זה" diff --git a/_static/locales/ja/LC_MESSAGES/booktheme.mo b/_static/locales/ja/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..1cefd29c Binary files /dev/null and b/_static/locales/ja/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ja/LC_MESSAGES/booktheme.po b/_static/locales/ja/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..77d5a097 --- /dev/null +++ b/_static/locales/ja/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ja\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "のテーマ" + +msgid "Open an issue" +msgstr "問題を報告" + +msgid "Contents" +msgstr "目次" + +msgid "Download notebook file" +msgstr "ノートブックファイルをダウンロード" + +msgid "Sphinx Book Theme" +msgstr "スフィンクスの本のテーマ" + +msgid "Fullscreen mode" +msgstr "全画面モード" + +msgid "Edit this page" +msgstr "このページを編集" + +msgid "By" +msgstr "著者" + +msgid "Copyright" +msgstr "Copyright" + +msgid "Source repository" +msgstr "ソースリポジトリ" + +msgid "previous page" +msgstr "前のページ" + +msgid "next page" +msgstr "次のページ" + +msgid "Toggle navigation" +msgstr "ナビゲーションを切り替え" + +msgid "repository" +msgstr "リポジトリ" + +msgid "suggest edit" +msgstr "編集を提案する" + +msgid "open issue" +msgstr "未解決の問題" + +msgid "Launch" +msgstr "起動" + +msgid "Print to PDF" +msgstr "PDFに印刷" + +msgid "By the" +msgstr "によって" + +msgid "Last updated on" +msgstr "最終更新日" + +msgid "Download source file" +msgstr "ソースファイルをダウンロード" + +msgid "Download this page" +msgstr "このページをダウンロード" diff --git a/_static/locales/ko/LC_MESSAGES/booktheme.mo b/_static/locales/ko/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..06c7ec93 Binary files /dev/null and b/_static/locales/ko/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ko/LC_MESSAGES/booktheme.po b/_static/locales/ko/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..6ee3d781 --- /dev/null +++ b/_static/locales/ko/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ko\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "테마별" + +msgid "Open an issue" +msgstr "이슈 열기" + +msgid "Contents" +msgstr "내용" + +msgid "Download notebook file" +msgstr "노트북 파일 다운로드" + +msgid "Sphinx Book Theme" +msgstr "스핑크스 도서 테마" + +msgid "Fullscreen mode" +msgstr "전체 화면으로보기" + +msgid "Edit this page" +msgstr "이 페이지 편집" + +msgid "By" +msgstr "으로" + +msgid "Copyright" +msgstr "저작권" + +msgid "Source repository" +msgstr "소스 저장소" + +msgid "previous page" +msgstr "이전 페이지" + +msgid "next page" +msgstr "다음 페이지" + +msgid "Toggle navigation" +msgstr "탐색 전환" + +msgid "repository" +msgstr "저장소" + +msgid "suggest edit" +msgstr "편집 제안" + +msgid "open issue" +msgstr "열린 문제" + +msgid "Launch" +msgstr "시작하다" + +msgid "Print to PDF" +msgstr "PDF로 인쇄" + +msgid "By the" +msgstr "에 의해" + +msgid "Last updated on" +msgstr "마지막 업데이트" + +msgid "Download source file" +msgstr "소스 파일 다운로드" + +msgid "Download this page" +msgstr "이 페이지 다운로드" diff --git a/_static/locales/lt/LC_MESSAGES/booktheme.mo b/_static/locales/lt/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..4468ba04 Binary files /dev/null and b/_static/locales/lt/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/lt/LC_MESSAGES/booktheme.po b/_static/locales/lt/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..01be2679 --- /dev/null +++ b/_static/locales/lt/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: lt\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema" + +msgid "Open an issue" +msgstr "Atidarykite problemą" + +msgid "Contents" +msgstr "Turinys" + +msgid "Download notebook file" +msgstr "Atsisiųsti nešiojamojo kompiuterio failą" + +msgid "Sphinx Book Theme" +msgstr "Sfinkso knygos tema" + +msgid "Fullscreen mode" +msgstr "Pilno ekrano režimas" + +msgid "Edit this page" +msgstr "Redaguoti šį puslapį" + +msgid "By" +msgstr "Iki" + +msgid "Copyright" +msgstr "Autorių teisės" + +msgid "Source repository" +msgstr "Šaltinio saugykla" + +msgid "previous page" +msgstr "Ankstesnis puslapis" + +msgid "next page" +msgstr "Kitas puslapis" + +msgid "Toggle navigation" +msgstr "Perjungti naršymą" + +msgid "repository" +msgstr "saugykla" + +msgid "suggest edit" +msgstr "pasiūlyti redaguoti" + +msgid "open issue" +msgstr "atviras klausimas" + +msgid "Launch" +msgstr "Paleiskite" + +msgid "Print to PDF" +msgstr "Spausdinti į PDF" + +msgid "By the" +msgstr "Prie" + +msgid "Last updated on" +msgstr "Paskutinį kartą atnaujinta" + +msgid "Download source file" +msgstr "Atsisiųsti šaltinio failą" + +msgid "Download this page" +msgstr "Atsisiųskite šį puslapį" diff --git a/_static/locales/lv/LC_MESSAGES/booktheme.mo b/_static/locales/lv/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..74aa4d89 Binary files /dev/null and b/_static/locales/lv/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/lv/LC_MESSAGES/booktheme.po b/_static/locales/lv/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..993a1e41 --- /dev/null +++ b/_static/locales/lv/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: lv\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Autora tēma" + +msgid "Open an issue" +msgstr "Atveriet problēmu" + +msgid "Contents" +msgstr "Saturs" + +msgid "Download notebook file" +msgstr "Lejupielādēt piezīmju grāmatiņu" + +msgid "Sphinx Book Theme" +msgstr "Sfinksa grāmatas tēma" + +msgid "Fullscreen mode" +msgstr "Pilnekrāna režīms" + +msgid "Edit this page" +msgstr "Rediģēt šo lapu" + +msgid "By" +msgstr "Autors" + +msgid "Copyright" +msgstr "Autortiesības" + +msgid "Source repository" +msgstr "Avota krātuve" + +msgid "previous page" +msgstr "iepriekšējā lapa" + +msgid "next page" +msgstr "nākamā lapaspuse" + +msgid "Toggle navigation" +msgstr "Pārslēgt navigāciju" + +msgid "repository" +msgstr "krātuve" + +msgid "suggest edit" +msgstr "ieteikt rediģēt" + +msgid "open issue" +msgstr "atklāts jautājums" + +msgid "Launch" +msgstr "Uzsākt" + +msgid "Print to PDF" +msgstr "Drukāt PDF formātā" + +msgid "By the" +msgstr "Ar" + +msgid "Last updated on" +msgstr "Pēdējoreiz atjaunināts" + +msgid "Download source file" +msgstr "Lejupielādēt avota failu" + +msgid "Download this page" +msgstr "Lejupielādējiet šo lapu" diff --git a/_static/locales/ml/LC_MESSAGES/booktheme.mo b/_static/locales/ml/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..2736e8fc Binary files /dev/null and b/_static/locales/ml/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ml/LC_MESSAGES/booktheme.po b/_static/locales/ml/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..81daf7c8 --- /dev/null +++ b/_static/locales/ml/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ml\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "പ്രമേയം" + +msgid "Open an issue" +msgstr "ഒരു പ്രശ്നം തുറക്കുക" + +msgid "Download notebook file" +msgstr "നോട്ട്ബുക്ക് ഫയൽ ഡൺലോഡ് ചെയ്യുക" + +msgid "Sphinx Book Theme" +msgstr "സ്ഫിങ്ക്സ് പുസ്തക തീം" + +msgid "Edit this page" +msgstr "ഈ പേജ് എഡിറ്റുചെയ്യുക" + +msgid "By" +msgstr "എഴുതിയത്" + +msgid "Copyright" +msgstr "പകർപ്പവകാശം" + +msgid "Source repository" +msgstr "ഉറവിട ശേഖരം" + +msgid "previous page" +msgstr "മുൻപത്തെ താൾ" + +msgid "next page" +msgstr "അടുത്ത പേജ്" + +msgid "Toggle navigation" +msgstr "നാവിഗേഷൻ ടോഗിൾ ചെയ്യുക" + +msgid "suggest edit" +msgstr "എഡിറ്റുചെയ്യാൻ നിർദ്ദേശിക്കുക" + +msgid "open issue" +msgstr "തുറന്ന പ്രശ്നം" + +msgid "Launch" +msgstr "സമാരംഭിക്കുക" + +msgid "Print to PDF" +msgstr "PDF- ലേക്ക് പ്രിന്റുചെയ്യുക" + +msgid "By the" +msgstr "എഴുതിയത്" + +msgid "Last updated on" +msgstr "അവസാനം അപ്‌ഡേറ്റുചെയ്‌തത്" + +msgid "Download source file" +msgstr "ഉറവിട ഫയൽ ഡൗൺലോഡുചെയ്യുക" + +msgid "Download this page" +msgstr "ഈ പേജ് ഡൗൺലോഡുചെയ്യുക" diff --git a/_static/locales/mr/LC_MESSAGES/booktheme.mo b/_static/locales/mr/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..fe530100 Binary files /dev/null and b/_static/locales/mr/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/mr/LC_MESSAGES/booktheme.po b/_static/locales/mr/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..fd857bff --- /dev/null +++ b/_static/locales/mr/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: mr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "द्वारा थीम" + +msgid "Open an issue" +msgstr "एक मुद्दा उघडा" + +msgid "Download notebook file" +msgstr "नोटबुक फाईल डाउनलोड करा" + +msgid "Sphinx Book Theme" +msgstr "स्फिंक्स बुक थीम" + +msgid "Edit this page" +msgstr "हे पृष्ठ संपादित करा" + +msgid "By" +msgstr "द्वारा" + +msgid "Copyright" +msgstr "कॉपीराइट" + +msgid "Source repository" +msgstr "स्त्रोत भांडार" + +msgid "previous page" +msgstr "मागील पान" + +msgid "next page" +msgstr "पुढील पृष्ठ" + +msgid "Toggle navigation" +msgstr "नेव्हिगेशन टॉगल करा" + +msgid "suggest edit" +msgstr "संपादन सुचवा" + +msgid "open issue" +msgstr "खुला मुद्दा" + +msgid "Launch" +msgstr "लाँच करा" + +msgid "Print to PDF" +msgstr "पीडीएफवर मुद्रित करा" + +msgid "By the" +msgstr "द्वारा" + +msgid "Last updated on" +msgstr "अखेरचे अद्यतनित" + +msgid "Download source file" +msgstr "स्त्रोत फाइल डाउनलोड करा" + +msgid "Download this page" +msgstr "हे पृष्ठ डाउनलोड करा" diff --git a/_static/locales/ms/LC_MESSAGES/booktheme.mo b/_static/locales/ms/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..f02603fa Binary files /dev/null and b/_static/locales/ms/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ms/LC_MESSAGES/booktheme.po b/_static/locales/ms/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..b616d70f --- /dev/null +++ b/_static/locales/ms/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ms\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema oleh" + +msgid "Open an issue" +msgstr "Buka masalah" + +msgid "Download notebook file" +msgstr "Muat turun fail buku nota" + +msgid "Sphinx Book Theme" +msgstr "Tema Buku Sphinx" + +msgid "Edit this page" +msgstr "Edit halaman ini" + +msgid "By" +msgstr "Oleh" + +msgid "Copyright" +msgstr "hak cipta" + +msgid "Source repository" +msgstr "Repositori sumber" + +msgid "previous page" +msgstr "halaman sebelumnya" + +msgid "next page" +msgstr "muka surat seterusnya" + +msgid "Toggle navigation" +msgstr "Togol navigasi" + +msgid "suggest edit" +msgstr "cadangkan edit" + +msgid "open issue" +msgstr "isu terbuka" + +msgid "Launch" +msgstr "Lancarkan" + +msgid "Print to PDF" +msgstr "Cetak ke PDF" + +msgid "By the" +msgstr "Oleh" + +msgid "Last updated on" +msgstr "Terakhir dikemas kini pada" + +msgid "Download source file" +msgstr "Muat turun fail sumber" + +msgid "Download this page" +msgstr "Muat turun halaman ini" diff --git a/_static/locales/nl/LC_MESSAGES/booktheme.mo b/_static/locales/nl/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..e59e7ecb Binary files /dev/null and b/_static/locales/nl/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/nl/LC_MESSAGES/booktheme.po b/_static/locales/nl/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..f16f4bcc --- /dev/null +++ b/_static/locales/nl/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: nl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Thema door de" + +msgid "Open an issue" +msgstr "Open een probleem" + +msgid "Contents" +msgstr "Inhoud" + +msgid "Download notebook file" +msgstr "Download notebookbestand" + +msgid "Sphinx Book Theme" +msgstr "Sphinx-boekthema" + +msgid "Fullscreen mode" +msgstr "Volledig scherm" + +msgid "Edit this page" +msgstr "bewerk deze pagina" + +msgid "By" +msgstr "Door" + +msgid "Copyright" +msgstr "auteursrechten" + +msgid "Source repository" +msgstr "Bronopslagplaats" + +msgid "previous page" +msgstr "vorige pagina" + +msgid "next page" +msgstr "volgende bladzijde" + +msgid "Toggle navigation" +msgstr "Schakel navigatie" + +msgid "repository" +msgstr "repository" + +msgid "suggest edit" +msgstr "suggereren bewerken" + +msgid "open issue" +msgstr "open probleem" + +msgid "Launch" +msgstr "Lancering" + +msgid "Print to PDF" +msgstr "Afdrukken naar pdf" + +msgid "By the" +msgstr "Door de" + +msgid "Last updated on" +msgstr "Laatst geupdate op" + +msgid "Download source file" +msgstr "Download het bronbestand" + +msgid "Download this page" +msgstr "Download deze pagina" diff --git a/_static/locales/no/LC_MESSAGES/booktheme.mo b/_static/locales/no/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..6cd15c88 Binary files /dev/null and b/_static/locales/no/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/no/LC_MESSAGES/booktheme.po b/_static/locales/no/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..b1d304ee --- /dev/null +++ b/_static/locales/no/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: no\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema av" + +msgid "Open an issue" +msgstr "Åpne et problem" + +msgid "Contents" +msgstr "Innhold" + +msgid "Download notebook file" +msgstr "Last ned notatbokfilen" + +msgid "Sphinx Book Theme" +msgstr "Sphinx boktema" + +msgid "Fullscreen mode" +msgstr "Fullskjerm-modus" + +msgid "Edit this page" +msgstr "Rediger denne siden" + +msgid "By" +msgstr "Av" + +msgid "Copyright" +msgstr "opphavsrett" + +msgid "Source repository" +msgstr "Kildedepot" + +msgid "previous page" +msgstr "forrige side" + +msgid "next page" +msgstr "neste side" + +msgid "Toggle navigation" +msgstr "Bytt navigasjon" + +msgid "repository" +msgstr "oppbevaringssted" + +msgid "suggest edit" +msgstr "foreslå redigering" + +msgid "open issue" +msgstr "åpent nummer" + +msgid "Launch" +msgstr "Start" + +msgid "Print to PDF" +msgstr "Skriv ut til PDF" + +msgid "By the" +msgstr "Ved" + +msgid "Last updated on" +msgstr "Sist oppdatert den" + +msgid "Download source file" +msgstr "Last ned kildefilen" + +msgid "Download this page" +msgstr "Last ned denne siden" diff --git a/_static/locales/pl/LC_MESSAGES/booktheme.mo b/_static/locales/pl/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..9ebb584f Binary files /dev/null and b/_static/locales/pl/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/pl/LC_MESSAGES/booktheme.po b/_static/locales/pl/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..80d2c896 --- /dev/null +++ b/_static/locales/pl/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Motyw autorstwa" + +msgid "Open an issue" +msgstr "Otwórz problem" + +msgid "Contents" +msgstr "Zawartość" + +msgid "Download notebook file" +msgstr "Pobierz plik notatnika" + +msgid "Sphinx Book Theme" +msgstr "Motyw książki Sphinx" + +msgid "Fullscreen mode" +msgstr "Pełny ekran" + +msgid "Edit this page" +msgstr "Edytuj tę strone" + +msgid "By" +msgstr "Przez" + +msgid "Copyright" +msgstr "prawa autorskie" + +msgid "Source repository" +msgstr "Repozytorium źródłowe" + +msgid "previous page" +msgstr "Poprzednia strona" + +msgid "next page" +msgstr "Następna strona" + +msgid "Toggle navigation" +msgstr "Przełącz nawigację" + +msgid "repository" +msgstr "magazyn" + +msgid "suggest edit" +msgstr "zaproponuj edycję" + +msgid "open issue" +msgstr "otwarty problem" + +msgid "Launch" +msgstr "Uruchomić" + +msgid "Print to PDF" +msgstr "Drukuj do PDF" + +msgid "By the" +msgstr "Przez" + +msgid "Last updated on" +msgstr "Ostatnia aktualizacja" + +msgid "Download source file" +msgstr "Pobierz plik źródłowy" + +msgid "Download this page" +msgstr "Pobierz tę stronę" diff --git a/_static/locales/pt/LC_MESSAGES/booktheme.mo b/_static/locales/pt/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..d0ddb872 Binary files /dev/null and b/_static/locales/pt/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/pt/LC_MESSAGES/booktheme.po b/_static/locales/pt/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..45ac847f --- /dev/null +++ b/_static/locales/pt/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema por" + +msgid "Open an issue" +msgstr "Abra um problema" + +msgid "Contents" +msgstr "Conteúdo" + +msgid "Download notebook file" +msgstr "Baixar arquivo de notebook" + +msgid "Sphinx Book Theme" +msgstr "Tema do livro Sphinx" + +msgid "Fullscreen mode" +msgstr "Modo tela cheia" + +msgid "Edit this page" +msgstr "Edite essa página" + +msgid "By" +msgstr "De" + +msgid "Copyright" +msgstr "direito autoral" + +msgid "Source repository" +msgstr "Repositório fonte" + +msgid "previous page" +msgstr "página anterior" + +msgid "next page" +msgstr "próxima página" + +msgid "Toggle navigation" +msgstr "Alternar de navegação" + +msgid "repository" +msgstr "repositório" + +msgid "suggest edit" +msgstr "sugerir edição" + +msgid "open issue" +msgstr "questão aberta" + +msgid "Launch" +msgstr "Lançamento" + +msgid "Print to PDF" +msgstr "Imprimir em PDF" + +msgid "By the" +msgstr "Pelo" + +msgid "Last updated on" +msgstr "Última atualização em" + +msgid "Download source file" +msgstr "Baixar arquivo fonte" + +msgid "Download this page" +msgstr "Baixe esta página" diff --git a/_static/locales/ro/LC_MESSAGES/booktheme.mo b/_static/locales/ro/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..3c36ab1d Binary files /dev/null and b/_static/locales/ro/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ro/LC_MESSAGES/booktheme.po b/_static/locales/ro/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..532b3b84 --- /dev/null +++ b/_static/locales/ro/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ro\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema de" + +msgid "Open an issue" +msgstr "Deschideți o problemă" + +msgid "Contents" +msgstr "Cuprins" + +msgid "Download notebook file" +msgstr "Descărcați fișierul notebook" + +msgid "Sphinx Book Theme" +msgstr "Tema Sphinx Book" + +msgid "Fullscreen mode" +msgstr "Modul ecran întreg" + +msgid "Edit this page" +msgstr "Editați această pagină" + +msgid "By" +msgstr "De" + +msgid "Copyright" +msgstr "Drepturi de autor" + +msgid "Source repository" +msgstr "Depozit sursă" + +msgid "previous page" +msgstr "pagina anterioară" + +msgid "next page" +msgstr "pagina următoare" + +msgid "Toggle navigation" +msgstr "Comutare navigare" + +msgid "repository" +msgstr "repertoriu" + +msgid "suggest edit" +msgstr "sugerează editare" + +msgid "open issue" +msgstr "problema deschisă" + +msgid "Launch" +msgstr "Lansa" + +msgid "Print to PDF" +msgstr "Imprimați în PDF" + +msgid "By the" +msgstr "Langa" + +msgid "Last updated on" +msgstr "Ultima actualizare la" + +msgid "Download source file" +msgstr "Descărcați fișierul sursă" + +msgid "Download this page" +msgstr "Descarcă această pagină" diff --git a/_static/locales/ru/LC_MESSAGES/booktheme.mo b/_static/locales/ru/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..6b8ca41f Binary files /dev/null and b/_static/locales/ru/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ru/LC_MESSAGES/booktheme.po b/_static/locales/ru/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..b718b482 --- /dev/null +++ b/_static/locales/ru/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ru\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Тема от" + +msgid "Open an issue" +msgstr "Открыть вопрос" + +msgid "Contents" +msgstr "Содержание" + +msgid "Download notebook file" +msgstr "Скачать файл записной книжки" + +msgid "Sphinx Book Theme" +msgstr "Тема книги Сфинкс" + +msgid "Fullscreen mode" +msgstr "Полноэкранный режим" + +msgid "Edit this page" +msgstr "Редактировать эту страницу" + +msgid "By" +msgstr "По" + +msgid "Copyright" +msgstr "авторское право" + +msgid "Source repository" +msgstr "Исходный репозиторий" + +msgid "previous page" +msgstr "Предыдущая страница" + +msgid "next page" +msgstr "Следующая страница" + +msgid "Toggle navigation" +msgstr "Переключить навигацию" + +msgid "repository" +msgstr "хранилище" + +msgid "suggest edit" +msgstr "предложить редактировать" + +msgid "open issue" +msgstr "открытый вопрос" + +msgid "Launch" +msgstr "Запуск" + +msgid "Print to PDF" +msgstr "Распечатать в PDF" + +msgid "By the" +msgstr "Посредством" + +msgid "Last updated on" +msgstr "Последнее обновление" + +msgid "Download source file" +msgstr "Скачать исходный файл" + +msgid "Download this page" +msgstr "Загрузите эту страницу" diff --git a/_static/locales/sk/LC_MESSAGES/booktheme.mo b/_static/locales/sk/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..59bd0ddf Binary files /dev/null and b/_static/locales/sk/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/sk/LC_MESSAGES/booktheme.po b/_static/locales/sk/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..f6c423b6 --- /dev/null +++ b/_static/locales/sk/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sk\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Téma od" + +msgid "Open an issue" +msgstr "Otvorte problém" + +msgid "Contents" +msgstr "Obsah" + +msgid "Download notebook file" +msgstr "Stiahnite si zošit" + +msgid "Sphinx Book Theme" +msgstr "Téma knihy Sfinga" + +msgid "Fullscreen mode" +msgstr "Režim celej obrazovky" + +msgid "Edit this page" +msgstr "Upraviť túto stránku" + +msgid "By" +msgstr "Autor:" + +msgid "Copyright" +msgstr "Autorské práva" + +msgid "Source repository" +msgstr "Zdrojové úložisko" + +msgid "previous page" +msgstr "predchádzajúca strana" + +msgid "next page" +msgstr "ďalšia strana" + +msgid "Toggle navigation" +msgstr "Prepnúť navigáciu" + +msgid "repository" +msgstr "Úložisko" + +msgid "suggest edit" +msgstr "navrhnúť úpravu" + +msgid "open issue" +msgstr "otvorené vydanie" + +msgid "Launch" +msgstr "Spustiť" + +msgid "Print to PDF" +msgstr "Tlač do PDF" + +msgid "By the" +msgstr "Podľa" + +msgid "Last updated on" +msgstr "Posledná aktualizácia dňa" + +msgid "Download source file" +msgstr "Stiahnite si zdrojový súbor" + +msgid "Download this page" +msgstr "Stiahnite si túto stránku" diff --git a/_static/locales/sl/LC_MESSAGES/booktheme.mo b/_static/locales/sl/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..87bf26de Binary files /dev/null and b/_static/locales/sl/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/sl/LC_MESSAGES/booktheme.po b/_static/locales/sl/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..9822dc58 --- /dev/null +++ b/_static/locales/sl/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema avtorja" + +msgid "Open an issue" +msgstr "Odprite številko" + +msgid "Contents" +msgstr "Vsebina" + +msgid "Download notebook file" +msgstr "Prenesite datoteko zvezka" + +msgid "Sphinx Book Theme" +msgstr "Tema knjige Sphinx" + +msgid "Fullscreen mode" +msgstr "Celozaslonski način" + +msgid "Edit this page" +msgstr "Uredite to stran" + +msgid "By" +msgstr "Avtor" + +msgid "Copyright" +msgstr "avtorske pravice" + +msgid "Source repository" +msgstr "Izvorno skladišče" + +msgid "previous page" +msgstr "Prejšnja stran" + +msgid "next page" +msgstr "Naslednja stran" + +msgid "Toggle navigation" +msgstr "Preklopi navigacijo" + +msgid "repository" +msgstr "odlagališče" + +msgid "suggest edit" +msgstr "predlagajte urejanje" + +msgid "open issue" +msgstr "odprto vprašanje" + +msgid "Launch" +msgstr "Kosilo" + +msgid "Print to PDF" +msgstr "Natisni v PDF" + +msgid "By the" +msgstr "Avtor" + +msgid "Last updated on" +msgstr "Nazadnje posodobljeno dne" + +msgid "Download source file" +msgstr "Prenesite izvorno datoteko" + +msgid "Download this page" +msgstr "Prenesite to stran" diff --git a/_static/locales/sr/LC_MESSAGES/booktheme.mo b/_static/locales/sr/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..ec740f48 Binary files /dev/null and b/_static/locales/sr/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/sr/LC_MESSAGES/booktheme.po b/_static/locales/sr/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..e809230c --- /dev/null +++ b/_static/locales/sr/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Тхеме би" + +msgid "Open an issue" +msgstr "Отворите издање" + +msgid "Contents" +msgstr "Садржај" + +msgid "Download notebook file" +msgstr "Преузмите датотеку бележнице" + +msgid "Sphinx Book Theme" +msgstr "Тема књиге Спхинк" + +msgid "Fullscreen mode" +msgstr "Режим целог екрана" + +msgid "Edit this page" +msgstr "Уредите ову страницу" + +msgid "By" +msgstr "Од стране" + +msgid "Copyright" +msgstr "Ауторско право" + +msgid "Source repository" +msgstr "Изворно спремиште" + +msgid "previous page" +msgstr "Претходна страница" + +msgid "next page" +msgstr "Следећа страна" + +msgid "Toggle navigation" +msgstr "Укључи / искључи навигацију" + +msgid "repository" +msgstr "спремиште" + +msgid "suggest edit" +msgstr "предложи уређивање" + +msgid "open issue" +msgstr "отворено издање" + +msgid "Launch" +msgstr "Лансирање" + +msgid "Print to PDF" +msgstr "Испис у ПДФ" + +msgid "By the" +msgstr "Од" + +msgid "Last updated on" +msgstr "Последње ажурирање" + +msgid "Download source file" +msgstr "Преузми изворну датотеку" + +msgid "Download this page" +msgstr "Преузмите ову страницу" diff --git a/_static/locales/sv/LC_MESSAGES/booktheme.mo b/_static/locales/sv/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..b07dc76f Binary files /dev/null and b/_static/locales/sv/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/sv/LC_MESSAGES/booktheme.po b/_static/locales/sv/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..2421b001 --- /dev/null +++ b/_static/locales/sv/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sv\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema av" + +msgid "Open an issue" +msgstr "Öppna en problemrapport" + +msgid "Contents" +msgstr "Innehåll" + +msgid "Download notebook file" +msgstr "Ladda ner notebook-fil" + +msgid "Sphinx Book Theme" +msgstr "Sphinx Boktema" + +msgid "Fullscreen mode" +msgstr "Fullskärmsläge" + +msgid "Edit this page" +msgstr "Redigera den här sidan" + +msgid "By" +msgstr "Av" + +msgid "Copyright" +msgstr "Upphovsrätt" + +msgid "Source repository" +msgstr "Källkodsrepositorium" + +msgid "previous page" +msgstr "föregående sida" + +msgid "next page" +msgstr "nästa sida" + +msgid "Toggle navigation" +msgstr "Växla navigering" + +msgid "repository" +msgstr "repositorium" + +msgid "suggest edit" +msgstr "föreslå ändring" + +msgid "open issue" +msgstr "öppna problemrapport" + +msgid "Launch" +msgstr "Öppna" + +msgid "Print to PDF" +msgstr "Skriv ut till PDF" + +msgid "By the" +msgstr "Av den" + +msgid "Last updated on" +msgstr "Senast uppdaterad den" + +msgid "Download source file" +msgstr "Ladda ner källfil" + +msgid "Download this page" +msgstr "Ladda ner den här sidan" diff --git a/_static/locales/ta/LC_MESSAGES/booktheme.mo b/_static/locales/ta/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..29f52e1f Binary files /dev/null and b/_static/locales/ta/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ta/LC_MESSAGES/booktheme.po b/_static/locales/ta/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..500042f4 --- /dev/null +++ b/_static/locales/ta/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ta\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "வழங்கிய தீம்" + +msgid "Open an issue" +msgstr "சிக்கலைத் திறக்கவும்" + +msgid "Download notebook file" +msgstr "நோட்புக் கோப்பைப் பதிவிறக்கவும்" + +msgid "Sphinx Book Theme" +msgstr "ஸ்பிங்க்ஸ் புத்தக தீம்" + +msgid "Edit this page" +msgstr "இந்தப் பக்கத்தைத் திருத்தவும்" + +msgid "By" +msgstr "வழங்கியவர்" + +msgid "Copyright" +msgstr "பதிப்புரிமை" + +msgid "Source repository" +msgstr "மூல களஞ்சியம்" + +msgid "previous page" +msgstr "முந்தைய பக்கம்" + +msgid "next page" +msgstr "அடுத்த பக்கம்" + +msgid "Toggle navigation" +msgstr "வழிசெலுத்தலை நிலைமாற்று" + +msgid "suggest edit" +msgstr "திருத்த பரிந்துரைக்கவும்" + +msgid "open issue" +msgstr "திறந்த பிரச்சினை" + +msgid "Launch" +msgstr "தொடங்க" + +msgid "Print to PDF" +msgstr "PDF இல் அச்சிடுக" + +msgid "By the" +msgstr "மூலம்" + +msgid "Last updated on" +msgstr "கடைசியாக புதுப்பிக்கப்பட்டது" + +msgid "Download source file" +msgstr "மூல கோப்பைப் பதிவிறக்குக" + +msgid "Download this page" +msgstr "இந்தப் பக்கத்தைப் பதிவிறக்கவும்" diff --git a/_static/locales/te/LC_MESSAGES/booktheme.mo b/_static/locales/te/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..0a5f4b46 Binary files /dev/null and b/_static/locales/te/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/te/LC_MESSAGES/booktheme.po b/_static/locales/te/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..b1afebba --- /dev/null +++ b/_static/locales/te/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: te\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "ద్వారా థీమ్" + +msgid "Open an issue" +msgstr "సమస్యను తెరవండి" + +msgid "Download notebook file" +msgstr "నోట్బుక్ ఫైల్ను డౌన్లోడ్ చేయండి" + +msgid "Sphinx Book Theme" +msgstr "సింహిక పుస్తక థీమ్" + +msgid "Edit this page" +msgstr "ఈ పేజీని సవరించండి" + +msgid "By" +msgstr "ద్వారా" + +msgid "Copyright" +msgstr "కాపీరైట్" + +msgid "Source repository" +msgstr "మూల రిపోజిటరీ" + +msgid "previous page" +msgstr "ముందు పేజి" + +msgid "next page" +msgstr "తరువాతి పేజీ" + +msgid "Toggle navigation" +msgstr "నావిగేషన్‌ను టోగుల్ చేయండి" + +msgid "suggest edit" +msgstr "సవరించమని సూచించండి" + +msgid "open issue" +msgstr "ఓపెన్ ఇష్యూ" + +msgid "Launch" +msgstr "ప్రారంభించండి" + +msgid "Print to PDF" +msgstr "PDF కి ముద్రించండి" + +msgid "By the" +msgstr "ద్వారా" + +msgid "Last updated on" +msgstr "చివరిగా నవీకరించబడింది" + +msgid "Download source file" +msgstr "మూల ఫైల్‌ను డౌన్‌లోడ్ చేయండి" + +msgid "Download this page" +msgstr "ఈ పేజీని డౌన్‌లోడ్ చేయండి" diff --git a/_static/locales/tg/LC_MESSAGES/booktheme.mo b/_static/locales/tg/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..b21c6c63 Binary files /dev/null and b/_static/locales/tg/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/tg/LC_MESSAGES/booktheme.po b/_static/locales/tg/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..29b8237b --- /dev/null +++ b/_static/locales/tg/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: tg\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Мавзӯъи аз" + +msgid "Open an issue" +msgstr "Масъаларо кушоед" + +msgid "Contents" +msgstr "Мундариҷа" + +msgid "Download notebook file" +msgstr "Файли дафтарро зеркашӣ кунед" + +msgid "Sphinx Book Theme" +msgstr "Сфинкс Мавзӯи китоб" + +msgid "Fullscreen mode" +msgstr "Ҳолати экрани пурра" + +msgid "Edit this page" +msgstr "Ин саҳифаро таҳрир кунед" + +msgid "By" +msgstr "Бо" + +msgid "Copyright" +msgstr "Ҳуқуқи муаллиф" + +msgid "Source repository" +msgstr "Анбори манбаъ" + +msgid "previous page" +msgstr "саҳифаи қаблӣ" + +msgid "next page" +msgstr "саҳифаи оянда" + +msgid "Toggle navigation" +msgstr "Гузаришро иваз кунед" + +msgid "repository" +msgstr "анбор" + +msgid "suggest edit" +msgstr "пешниҳод вироиш" + +msgid "open issue" +msgstr "барориши кушод" + +msgid "Launch" +msgstr "Оғоз" + +msgid "Print to PDF" +msgstr "Чоп ба PDF" + +msgid "By the" +msgstr "Бо" + +msgid "Last updated on" +msgstr "Last навсозӣ дар" + +msgid "Download source file" +msgstr "Файли манбаъро зеркашӣ кунед" + +msgid "Download this page" +msgstr "Ин саҳифаро зеркашӣ кунед" diff --git a/_static/locales/th/LC_MESSAGES/booktheme.mo b/_static/locales/th/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..abede98a Binary files /dev/null and b/_static/locales/th/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/th/LC_MESSAGES/booktheme.po b/_static/locales/th/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..ac65ee05 --- /dev/null +++ b/_static/locales/th/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: th\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "ธีมโดย" + +msgid "Open an issue" +msgstr "เปิดปัญหา" + +msgid "Contents" +msgstr "สารบัญ" + +msgid "Download notebook file" +msgstr "ดาวน์โหลดไฟล์สมุดบันทึก" + +msgid "Sphinx Book Theme" +msgstr "ธีมหนังสือสฟิงซ์" + +msgid "Fullscreen mode" +msgstr "โหมดเต็มหน้าจอ" + +msgid "Edit this page" +msgstr "แก้ไขหน้านี้" + +msgid "By" +msgstr "โดย" + +msgid "Copyright" +msgstr "ลิขสิทธิ์" + +msgid "Source repository" +msgstr "ที่เก็บซอร์ส" + +msgid "previous page" +msgstr "หน้าที่แล้ว" + +msgid "next page" +msgstr "หน้าต่อไป" + +msgid "Toggle navigation" +msgstr "ไม่ต้องสลับช่องทาง" + +msgid "repository" +msgstr "ที่เก็บ" + +msgid "suggest edit" +msgstr "แนะนำแก้ไข" + +msgid "open issue" +msgstr "เปิดปัญหา" + +msgid "Launch" +msgstr "เปิด" + +msgid "Print to PDF" +msgstr "พิมพ์เป็น PDF" + +msgid "By the" +msgstr "โดย" + +msgid "Last updated on" +msgstr "ปรับปรุงล่าสุดเมื่อ" + +msgid "Download source file" +msgstr "ดาวน์โหลดไฟล์ต้นฉบับ" + +msgid "Download this page" +msgstr "ดาวน์โหลดหน้านี้" diff --git a/_static/locales/tl/LC_MESSAGES/booktheme.mo b/_static/locales/tl/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..8df1b733 Binary files /dev/null and b/_static/locales/tl/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/tl/LC_MESSAGES/booktheme.po b/_static/locales/tl/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..662d66ca --- /dev/null +++ b/_static/locales/tl/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: tl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tema ng" + +msgid "Open an issue" +msgstr "Magbukas ng isyu" + +msgid "Download notebook file" +msgstr "Mag-download ng file ng notebook" + +msgid "Sphinx Book Theme" +msgstr "Tema ng Sphinx Book" + +msgid "Edit this page" +msgstr "I-edit ang pahinang ito" + +msgid "By" +msgstr "Ni" + +msgid "Copyright" +msgstr "Copyright" + +msgid "Source repository" +msgstr "Pinagmulan ng imbakan" + +msgid "previous page" +msgstr "Nakaraang pahina" + +msgid "next page" +msgstr "Susunod na pahina" + +msgid "Toggle navigation" +msgstr "I-toggle ang pag-navigate" + +msgid "suggest edit" +msgstr "iminumungkahi i-edit" + +msgid "open issue" +msgstr "bukas na isyu" + +msgid "Launch" +msgstr "Ilunsad" + +msgid "Print to PDF" +msgstr "I-print sa PDF" + +msgid "By the" +msgstr "Sa pamamagitan ng" + +msgid "Last updated on" +msgstr "Huling na-update noong" + +msgid "Download source file" +msgstr "Mag-download ng file ng pinagmulan" + +msgid "Download this page" +msgstr "I-download ang pahinang ito" diff --git a/_static/locales/tr/LC_MESSAGES/booktheme.mo b/_static/locales/tr/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..029ae18a Binary files /dev/null and b/_static/locales/tr/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/tr/LC_MESSAGES/booktheme.po b/_static/locales/tr/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..d1ae7233 --- /dev/null +++ b/_static/locales/tr/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: tr\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Tarafından tema" + +msgid "Open an issue" +msgstr "Bir sorunu açın" + +msgid "Contents" +msgstr "İçindekiler" + +msgid "Download notebook file" +msgstr "Defter dosyasını indirin" + +msgid "Sphinx Book Theme" +msgstr "Sfenks Kitap Teması" + +msgid "Fullscreen mode" +msgstr "Tam ekran modu" + +msgid "Edit this page" +msgstr "Bu sayfayı düzenle" + +msgid "By" +msgstr "Tarafından" + +msgid "Copyright" +msgstr "Telif hakkı" + +msgid "Source repository" +msgstr "Kaynak kod deposu" + +msgid "previous page" +msgstr "önceki sayfa" + +msgid "next page" +msgstr "sonraki Sayfa" + +msgid "Toggle navigation" +msgstr "Gezinmeyi değiştir" + +msgid "repository" +msgstr "depo" + +msgid "suggest edit" +msgstr "düzenleme öner" + +msgid "open issue" +msgstr "Açık konu" + +msgid "Launch" +msgstr "Başlatmak" + +msgid "Print to PDF" +msgstr "PDF olarak yazdır" + +msgid "By the" +msgstr "Tarafından" + +msgid "Last updated on" +msgstr "Son güncelleme tarihi" + +msgid "Download source file" +msgstr "Kaynak dosyayı indirin" + +msgid "Download this page" +msgstr "Bu sayfayı indirin" diff --git a/_static/locales/uk/LC_MESSAGES/booktheme.mo b/_static/locales/uk/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..16ab7890 Binary files /dev/null and b/_static/locales/uk/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/uk/LC_MESSAGES/booktheme.po b/_static/locales/uk/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..be49ab85 --- /dev/null +++ b/_static/locales/uk/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: uk\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Тема від" + +msgid "Open an issue" +msgstr "Відкрийте випуск" + +msgid "Contents" +msgstr "Зміст" + +msgid "Download notebook file" +msgstr "Завантажте файл блокнота" + +msgid "Sphinx Book Theme" +msgstr "Тема книги \"Сфінкс\"" + +msgid "Fullscreen mode" +msgstr "Повноекранний режим" + +msgid "Edit this page" +msgstr "Редагувати цю сторінку" + +msgid "By" +msgstr "Автор" + +msgid "Copyright" +msgstr "Авторське право" + +msgid "Source repository" +msgstr "Джерело сховища" + +msgid "previous page" +msgstr "Попередня сторінка" + +msgid "next page" +msgstr "Наступна сторінка" + +msgid "Toggle navigation" +msgstr "Переключити навігацію" + +msgid "repository" +msgstr "сховище" + +msgid "suggest edit" +msgstr "запропонувати редагувати" + +msgid "open issue" +msgstr "відкритий випуск" + +msgid "Launch" +msgstr "Запуск" + +msgid "Print to PDF" +msgstr "Друк у форматі PDF" + +msgid "By the" +msgstr "По" + +msgid "Last updated on" +msgstr "Останнє оновлення:" + +msgid "Download source file" +msgstr "Завантажити вихідний файл" + +msgid "Download this page" +msgstr "Завантажте цю сторінку" diff --git a/_static/locales/ur/LC_MESSAGES/booktheme.mo b/_static/locales/ur/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..de8c84b9 Binary files /dev/null and b/_static/locales/ur/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/ur/LC_MESSAGES/booktheme.po b/_static/locales/ur/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..94bcab33 --- /dev/null +++ b/_static/locales/ur/LC_MESSAGES/booktheme.po @@ -0,0 +1,66 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ur\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "کے ذریعہ تھیم" + +msgid "Open an issue" +msgstr "ایک مسئلہ کھولیں" + +msgid "Download notebook file" +msgstr "نوٹ بک فائل ڈاؤن لوڈ کریں" + +msgid "Sphinx Book Theme" +msgstr "سپنکس بک تھیم" + +msgid "Edit this page" +msgstr "اس صفحے میں ترمیم کریں" + +msgid "By" +msgstr "بذریعہ" + +msgid "Copyright" +msgstr "کاپی رائٹ" + +msgid "Source repository" +msgstr "ماخذ ذخیرہ" + +msgid "previous page" +msgstr "سابقہ ​​صفحہ" + +msgid "next page" +msgstr "اگلا صفحہ" + +msgid "Toggle navigation" +msgstr "نیویگیشن ٹوگل کریں" + +msgid "suggest edit" +msgstr "ترمیم کی تجویز کریں" + +msgid "open issue" +msgstr "کھلا مسئلہ" + +msgid "Launch" +msgstr "لانچ کریں" + +msgid "Print to PDF" +msgstr "پی ڈی ایف پرنٹ کریں" + +msgid "By the" +msgstr "کی طرف" + +msgid "Last updated on" +msgstr "آخری بار تازہ کاری ہوئی" + +msgid "Download source file" +msgstr "سورس فائل ڈاؤن لوڈ کریں" + +msgid "Download this page" +msgstr "اس صفحے کو ڈاؤن لوڈ کریں" diff --git a/_static/locales/vi/LC_MESSAGES/booktheme.mo b/_static/locales/vi/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..2bb32555 Binary files /dev/null and b/_static/locales/vi/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/vi/LC_MESSAGES/booktheme.po b/_static/locales/vi/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..116236dc --- /dev/null +++ b/_static/locales/vi/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: vi\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "Chủ đề của" + +msgid "Open an issue" +msgstr "Mở một vấn đề" + +msgid "Contents" +msgstr "Nội dung" + +msgid "Download notebook file" +msgstr "Tải xuống tệp sổ tay" + +msgid "Sphinx Book Theme" +msgstr "Chủ đề sách nhân sư" + +msgid "Fullscreen mode" +msgstr "Chế độ toàn màn hình" + +msgid "Edit this page" +msgstr "chỉnh sửa trang này" + +msgid "By" +msgstr "Bởi" + +msgid "Copyright" +msgstr "Bản quyền" + +msgid "Source repository" +msgstr "Kho nguồn" + +msgid "previous page" +msgstr "trang trước" + +msgid "next page" +msgstr "Trang tiếp theo" + +msgid "Toggle navigation" +msgstr "Chuyển đổi điều hướng thành" + +msgid "repository" +msgstr "kho" + +msgid "suggest edit" +msgstr "đề nghị chỉnh sửa" + +msgid "open issue" +msgstr "vấn đề mở" + +msgid "Launch" +msgstr "Phóng" + +msgid "Print to PDF" +msgstr "In sang PDF" + +msgid "By the" +msgstr "Bằng" + +msgid "Last updated on" +msgstr "Cập nhật lần cuối vào" + +msgid "Download source file" +msgstr "Tải xuống tệp nguồn" + +msgid "Download this page" +msgstr "Tải xuống trang này" diff --git a/_static/locales/zh_CN/LC_MESSAGES/booktheme.mo b/_static/locales/zh_CN/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..0e3235d0 Binary files /dev/null and b/_static/locales/zh_CN/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/zh_CN/LC_MESSAGES/booktheme.po b/_static/locales/zh_CN/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..4f4ab579 --- /dev/null +++ b/_static/locales/zh_CN/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_CN\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "主题作者:" + +msgid "Open an issue" +msgstr "创建议题" + +msgid "Contents" +msgstr "目录" + +msgid "Download notebook file" +msgstr "下载笔记本文件" + +msgid "Sphinx Book Theme" +msgstr "Sphinx Book 主题" + +msgid "Fullscreen mode" +msgstr "全屏模式" + +msgid "Edit this page" +msgstr "编辑此页面" + +msgid "By" +msgstr "作者:" + +msgid "Copyright" +msgstr "版权" + +msgid "Source repository" +msgstr "源码库" + +msgid "previous page" +msgstr "上一页" + +msgid "next page" +msgstr "下一页" + +msgid "Toggle navigation" +msgstr "显示或隐藏导航栏" + +msgid "repository" +msgstr "仓库" + +msgid "suggest edit" +msgstr "提出修改建议" + +msgid "open issue" +msgstr "创建议题" + +msgid "Launch" +msgstr "启动" + +msgid "Print to PDF" +msgstr "列印成 PDF" + +msgid "By the" +msgstr "作者:" + +msgid "Last updated on" +msgstr "上次更新时间:" + +msgid "Download source file" +msgstr "下载源文件" + +msgid "Download this page" +msgstr "下载此页面" diff --git a/_static/locales/zh_TW/LC_MESSAGES/booktheme.mo b/_static/locales/zh_TW/LC_MESSAGES/booktheme.mo new file mode 100644 index 00000000..9116fa95 Binary files /dev/null and b/_static/locales/zh_TW/LC_MESSAGES/booktheme.mo differ diff --git a/_static/locales/zh_TW/LC_MESSAGES/booktheme.po b/_static/locales/zh_TW/LC_MESSAGES/booktheme.po new file mode 100644 index 00000000..42b43b86 --- /dev/null +++ b/_static/locales/zh_TW/LC_MESSAGES/booktheme.po @@ -0,0 +1,75 @@ + +msgid "" +msgstr "" +"Project-Id-Version: Sphinx-Book-Theme\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_TW\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Theme by the" +msgstr "佈景主題作者:" + +msgid "Open an issue" +msgstr "開啟議題" + +msgid "Contents" +msgstr "目錄" + +msgid "Download notebook file" +msgstr "下載 Notebook 檔案" + +msgid "Sphinx Book Theme" +msgstr "Sphinx Book 佈景主題" + +msgid "Fullscreen mode" +msgstr "全螢幕模式" + +msgid "Edit this page" +msgstr "編輯此頁面" + +msgid "By" +msgstr "作者:" + +msgid "Copyright" +msgstr "Copyright" + +msgid "Source repository" +msgstr "來源儲存庫" + +msgid "previous page" +msgstr "上一頁" + +msgid "next page" +msgstr "下一頁" + +msgid "Toggle navigation" +msgstr "顯示或隱藏導覽列" + +msgid "repository" +msgstr "儲存庫" + +msgid "suggest edit" +msgstr "提出修改建議" + +msgid "open issue" +msgstr "公開的問題" + +msgid "Launch" +msgstr "啟動" + +msgid "Print to PDF" +msgstr "列印成 PDF" + +msgid "By the" +msgstr "作者:" + +msgid "Last updated on" +msgstr "最後更新時間:" + +msgid "Download source file" +msgstr "下載原始檔" + +msgid "Download this page" +msgstr "下載此頁面" diff --git a/_static/minus.png b/_static/minus.png new file mode 100644 index 00000000..d96755fd Binary files /dev/null and b/_static/minus.png differ diff --git a/_static/mystnb.4510f1fc1dee50b3e5859aac5469c37c29e427902b24a333a5f9fcb2f0b3ac41.css b/_static/mystnb.4510f1fc1dee50b3e5859aac5469c37c29e427902b24a333a5f9fcb2f0b3ac41.css new file mode 100644 index 00000000..33566310 --- /dev/null +++ b/_static/mystnb.4510f1fc1dee50b3e5859aac5469c37c29e427902b24a333a5f9fcb2f0b3ac41.css @@ -0,0 +1,2342 @@ +/* Variables */ +:root { + --mystnb-source-bg-color: #f7f7f7; + --mystnb-stdout-bg-color: #fcfcfc; + --mystnb-stderr-bg-color: #fdd; + --mystnb-traceback-bg-color: #fcfcfc; + --mystnb-source-border-color: #ccc; + --mystnb-source-margin-color: green; + --mystnb-stdout-border-color: #f7f7f7; + --mystnb-stderr-border-color: #f7f7f7; + --mystnb-traceback-border-color: #ffd6d6; + --mystnb-hide-prompt-opacity: 70%; + --mystnb-source-border-radius: .4em; + --mystnb-source-border-width: 1px; +} + +/* Whole cell */ +div.container.cell { + padding-left: 0; + margin-bottom: 1em; +} + +/* Removing all background formatting so we can control at the div level */ +.cell_input div.highlight, +.cell_output pre, +.cell_input pre, +.cell_output .output { + border: none; + box-shadow: none; +} + +.cell_output .output pre, +.cell_input pre { + margin: 0px; +} + +/* Input cells */ +div.cell div.cell_input, +div.cell details.above-input>summary { + padding-left: 0em; + padding-right: 0em; + border: var(--mystnb-source-border-width) var(--mystnb-source-border-color) solid; + background-color: var(--mystnb-source-bg-color); + border-left-color: var(--mystnb-source-margin-color); + border-left-width: medium; + border-radius: var(--mystnb-source-border-radius); +} + +div.cell_input>div, +div.cell_output div.output>div.highlight { + margin: 0em !important; + border: none !important; +} + +/* All cell outputs */ +.cell_output { + padding-left: 1em; + padding-right: 0em; + margin-top: 1em; +} + +/* Text outputs from cells */ +.cell_output .output.text_plain, +.cell_output .output.traceback, +.cell_output .output.stream, +.cell_output .output.stderr { + margin-top: 1em; + margin-bottom: 0em; + box-shadow: none; +} + +.cell_output .output.text_plain, +.cell_output .output.stream { + background: var(--mystnb-stdout-bg-color); + border: 1px solid var(--mystnb-stdout-border-color); +} + +.cell_output .output.stderr { + background: var(--mystnb-stderr-bg-color); + border: 1px solid var(--mystnb-stderr-border-color); +} + +.cell_output .output.traceback { + background: var(--mystnb-traceback-bg-color); + border: 1px solid var(--mystnb-traceback-border-color); +} + +/* Collapsible cell content */ +div.cell details.above-input div.cell_input { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-top: var(--mystnb-source-border-width) var(--mystnb-source-border-color) dashed; +} + +div.cell div.cell_input.above-output-prompt { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +div.cell details.above-input>summary { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-bottom: var(--mystnb-source-border-width) var(--mystnb-source-border-color) dashed; + padding-left: 1em; + margin-bottom: 0; +} + +div.cell details.above-output>summary { + background-color: var(--mystnb-source-bg-color); + padding-left: 1em; + padding-right: 0em; + border: var(--mystnb-source-border-width) var(--mystnb-source-border-color) solid; + border-radius: var(--mystnb-source-border-radius); + border-left-color: var(--mystnb-source-margin-color); + border-left-width: medium; +} + +div.cell details.below-input>summary { + background-color: var(--mystnb-source-bg-color); + padding-left: 1em; + padding-right: 0em; + border: var(--mystnb-source-border-width) var(--mystnb-source-border-color) solid; + border-top: none; + border-bottom-left-radius: var(--mystnb-source-border-radius); + border-bottom-right-radius: var(--mystnb-source-border-radius); + border-left-color: var(--mystnb-source-margin-color); + border-left-width: medium; +} + +div.cell details.hide>summary>span { + opacity: var(--mystnb-hide-prompt-opacity); +} + +div.cell details.hide[open]>summary>span.collapsed { + display: none; +} + +div.cell details.hide:not([open])>summary>span.expanded { + display: none; +} + +@keyframes collapsed-fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} +div.cell details.hide[open]>summary~* { + -moz-animation: collapsed-fade-in 0.3s ease-in-out; + -webkit-animation: collapsed-fade-in 0.3s ease-in-out; + animation: collapsed-fade-in 0.3s ease-in-out; +} + +/* Math align to the left */ +.cell_output .MathJax_Display { + text-align: left !important; +} + +/* Pandas tables. Pulled from the Jupyter / nbsphinx CSS */ +div.cell_output table { + border: none; + border-collapse: collapse; + border-spacing: 0; + color: black; + font-size: 1em; + table-layout: fixed; +} + +div.cell_output thead { + border-bottom: 1px solid black; + vertical-align: bottom; +} + +div.cell_output tr, +div.cell_output th, +div.cell_output td { + text-align: right; + vertical-align: middle; + padding: 0.5em 0.5em; + line-height: normal; + white-space: normal; + max-width: none; + border: none; +} + +div.cell_output th { + font-weight: bold; +} + +div.cell_output tbody tr:nth-child(odd) { + background: #f5f5f5; +} + +div.cell_output tbody tr:hover { + background: rgba(66, 165, 245, 0.2); +} + +/** source code line numbers **/ +span.linenos { + opacity: 0.5; +} + +/* Inline text from `paste` operation */ + +span.pasted-text { + font-weight: bold; +} + +span.pasted-inline img { + max-height: 2em; +} + +tbody span.pasted-inline img { + max-height: none; +} + +/* Font colors for translated ANSI escape sequences +Color values are copied from Jupyter Notebook +https://github.com/jupyter/notebook/blob/52581f8eda9b319eb0390ac77fe5903c38f81e3e/notebook/static/notebook/less/ansicolors.less#L14-L21 +Background colors from +https://nbsphinx.readthedocs.io/en/latest/code-cells.html#ANSI-Colors +*/ +div.highlight .-Color-Bold { + font-weight: bold; +} + +div.highlight .-Color[class*=-Black] { + color: #3E424D +} + +div.highlight .-Color[class*=-Red] { + color: #E75C58 +} + +div.highlight .-Color[class*=-Green] { + color: #00A250 +} + +div.highlight .-Color[class*=-Yellow] { + color: #DDB62B +} + +div.highlight .-Color[class*=-Blue] { + color: #208FFB +} + +div.highlight .-Color[class*=-Magenta] { + color: #D160C4 +} + +div.highlight .-Color[class*=-Cyan] { + color: #60C6C8 +} + +div.highlight .-Color[class*=-White] { + color: #C5C1B4 +} + +div.highlight .-Color[class*=-BGBlack] { + background-color: #3E424D +} + +div.highlight .-Color[class*=-BGRed] { + background-color: #E75C58 +} + +div.highlight .-Color[class*=-BGGreen] { + background-color: #00A250 +} + +div.highlight .-Color[class*=-BGYellow] { + background-color: #DDB62B +} + +div.highlight .-Color[class*=-BGBlue] { + background-color: #208FFB +} + +div.highlight .-Color[class*=-BGMagenta] { + background-color: #D160C4 +} + +div.highlight .-Color[class*=-BGCyan] { + background-color: #60C6C8 +} + +div.highlight .-Color[class*=-BGWhite] { + background-color: #C5C1B4 +} + +/* Font colors for 8-bit ANSI */ + +div.highlight .-Color[class*=-C0] { + color: #000000 +} + +div.highlight .-Color[class*=-BGC0] { + background-color: #000000 +} + +div.highlight .-Color[class*=-C1] { + color: #800000 +} + +div.highlight .-Color[class*=-BGC1] { + background-color: #800000 +} + +div.highlight .-Color[class*=-C2] { + color: #008000 +} + +div.highlight .-Color[class*=-BGC2] { + background-color: #008000 +} + +div.highlight .-Color[class*=-C3] { + color: #808000 +} + +div.highlight .-Color[class*=-BGC3] { + background-color: #808000 +} + +div.highlight .-Color[class*=-C4] { + color: #000080 +} + +div.highlight .-Color[class*=-BGC4] { + background-color: #000080 +} + +div.highlight .-Color[class*=-C5] { + color: #800080 +} + +div.highlight .-Color[class*=-BGC5] { + background-color: #800080 +} + +div.highlight .-Color[class*=-C6] { + color: #008080 +} + +div.highlight .-Color[class*=-BGC6] { + background-color: #008080 +} + +div.highlight .-Color[class*=-C7] { + color: #C0C0C0 +} + +div.highlight .-Color[class*=-BGC7] { + background-color: #C0C0C0 +} + +div.highlight .-Color[class*=-C8] { + color: #808080 +} + +div.highlight .-Color[class*=-BGC8] { + background-color: #808080 +} + +div.highlight .-Color[class*=-C9] { + color: #FF0000 +} + +div.highlight .-Color[class*=-BGC9] { + background-color: #FF0000 +} + +div.highlight .-Color[class*=-C10] { + color: #00FF00 +} + +div.highlight .-Color[class*=-BGC10] { + background-color: #00FF00 +} + +div.highlight .-Color[class*=-C11] { + color: #FFFF00 +} + +div.highlight .-Color[class*=-BGC11] { + background-color: #FFFF00 +} + +div.highlight .-Color[class*=-C12] { + color: #0000FF +} + +div.highlight .-Color[class*=-BGC12] { + background-color: #0000FF +} + +div.highlight .-Color[class*=-C13] { + color: #FF00FF +} + +div.highlight .-Color[class*=-BGC13] { + background-color: #FF00FF +} + +div.highlight .-Color[class*=-C14] { + color: #00FFFF +} + +div.highlight .-Color[class*=-BGC14] { + background-color: #00FFFF +} + +div.highlight .-Color[class*=-C15] { + color: #FFFFFF +} + +div.highlight .-Color[class*=-BGC15] { + background-color: #FFFFFF +} + +div.highlight .-Color[class*=-C16] { + color: #000000 +} + +div.highlight .-Color[class*=-BGC16] { + background-color: #000000 +} + +div.highlight .-Color[class*=-C17] { + color: #00005F +} + +div.highlight .-Color[class*=-BGC17] { + background-color: #00005F +} + +div.highlight .-Color[class*=-C18] { + color: #000087 +} + +div.highlight .-Color[class*=-BGC18] { + background-color: #000087 +} + +div.highlight .-Color[class*=-C19] { + color: #0000AF +} + +div.highlight .-Color[class*=-BGC19] { + background-color: #0000AF +} + +div.highlight .-Color[class*=-C20] { + color: #0000D7 +} + +div.highlight .-Color[class*=-BGC20] { + background-color: #0000D7 +} + +div.highlight .-Color[class*=-C21] { + color: #0000FF +} + +div.highlight .-Color[class*=-BGC21] { + background-color: #0000FF +} + +div.highlight .-Color[class*=-C22] { + color: #005F00 +} + +div.highlight .-Color[class*=-BGC22] { + background-color: #005F00 +} + +div.highlight .-Color[class*=-C23] { + color: #005F5F +} + +div.highlight .-Color[class*=-BGC23] { + background-color: #005F5F +} + +div.highlight .-Color[class*=-C24] { + color: #005F87 +} + +div.highlight .-Color[class*=-BGC24] { + background-color: #005F87 +} + +div.highlight .-Color[class*=-C25] { + color: #005FAF +} + +div.highlight .-Color[class*=-BGC25] { + background-color: #005FAF +} + +div.highlight .-Color[class*=-C26] { + color: #005FD7 +} + +div.highlight .-Color[class*=-BGC26] { + background-color: #005FD7 +} + +div.highlight .-Color[class*=-C27] { + color: #005FFF +} + +div.highlight .-Color[class*=-BGC27] { + background-color: #005FFF +} + +div.highlight .-Color[class*=-C28] { + color: #008700 +} + +div.highlight .-Color[class*=-BGC28] { + background-color: #008700 +} + +div.highlight .-Color[class*=-C29] { + color: #00875F +} + +div.highlight .-Color[class*=-BGC29] { + background-color: #00875F +} + +div.highlight .-Color[class*=-C30] { + color: #008787 +} + +div.highlight .-Color[class*=-BGC30] { + background-color: #008787 +} + +div.highlight .-Color[class*=-C31] { + color: #0087AF +} + +div.highlight .-Color[class*=-BGC31] { + background-color: #0087AF +} + +div.highlight .-Color[class*=-C32] { + color: #0087D7 +} + +div.highlight .-Color[class*=-BGC32] { + background-color: #0087D7 +} + +div.highlight .-Color[class*=-C33] { + color: #0087FF +} + +div.highlight .-Color[class*=-BGC33] { + background-color: #0087FF +} + +div.highlight .-Color[class*=-C34] { + color: #00AF00 +} + +div.highlight .-Color[class*=-BGC34] { + background-color: #00AF00 +} + +div.highlight .-Color[class*=-C35] { + color: #00AF5F +} + +div.highlight .-Color[class*=-BGC35] { + background-color: #00AF5F +} + +div.highlight .-Color[class*=-C36] { + color: #00AF87 +} + +div.highlight .-Color[class*=-BGC36] { + background-color: #00AF87 +} + +div.highlight .-Color[class*=-C37] { + color: #00AFAF +} + +div.highlight .-Color[class*=-BGC37] { + background-color: #00AFAF +} + +div.highlight .-Color[class*=-C38] { + color: #00AFD7 +} + +div.highlight .-Color[class*=-BGC38] { + background-color: #00AFD7 +} + +div.highlight .-Color[class*=-C39] { + color: #00AFFF +} + +div.highlight .-Color[class*=-BGC39] { + background-color: #00AFFF +} + +div.highlight .-Color[class*=-C40] { + color: #00D700 +} + +div.highlight .-Color[class*=-BGC40] { + background-color: #00D700 +} + +div.highlight .-Color[class*=-C41] { + color: #00D75F +} + +div.highlight .-Color[class*=-BGC41] { + background-color: #00D75F +} + +div.highlight .-Color[class*=-C42] { + color: #00D787 +} + +div.highlight .-Color[class*=-BGC42] { + background-color: #00D787 +} + +div.highlight .-Color[class*=-C43] { + color: #00D7AF +} + +div.highlight .-Color[class*=-BGC43] { + background-color: #00D7AF +} + +div.highlight .-Color[class*=-C44] { + color: #00D7D7 +} + +div.highlight .-Color[class*=-BGC44] { + background-color: #00D7D7 +} + +div.highlight .-Color[class*=-C45] { + color: #00D7FF +} + +div.highlight .-Color[class*=-BGC45] { + background-color: #00D7FF +} + +div.highlight .-Color[class*=-C46] { + color: #00FF00 +} + +div.highlight .-Color[class*=-BGC46] { + background-color: #00FF00 +} + +div.highlight .-Color[class*=-C47] { + color: #00FF5F +} + +div.highlight .-Color[class*=-BGC47] { + background-color: #00FF5F +} + +div.highlight .-Color[class*=-C48] { + color: #00FF87 +} + +div.highlight .-Color[class*=-BGC48] { + background-color: #00FF87 +} + +div.highlight .-Color[class*=-C49] { + color: #00FFAF +} + +div.highlight .-Color[class*=-BGC49] { + background-color: #00FFAF +} + +div.highlight .-Color[class*=-C50] { + color: #00FFD7 +} + +div.highlight .-Color[class*=-BGC50] { + background-color: #00FFD7 +} + +div.highlight .-Color[class*=-C51] { + color: #00FFFF +} + +div.highlight .-Color[class*=-BGC51] { + background-color: #00FFFF +} + +div.highlight .-Color[class*=-C52] { + color: #5F0000 +} + +div.highlight .-Color[class*=-BGC52] { + background-color: #5F0000 +} + +div.highlight .-Color[class*=-C53] { + color: #5F005F +} + +div.highlight .-Color[class*=-BGC53] { + background-color: #5F005F +} + +div.highlight .-Color[class*=-C54] { + color: #5F0087 +} + +div.highlight .-Color[class*=-BGC54] { + background-color: #5F0087 +} + +div.highlight .-Color[class*=-C55] { + color: #5F00AF +} + +div.highlight .-Color[class*=-BGC55] { + background-color: #5F00AF +} + +div.highlight .-Color[class*=-C56] { + color: #5F00D7 +} + +div.highlight .-Color[class*=-BGC56] { + background-color: #5F00D7 +} + +div.highlight .-Color[class*=-C57] { + color: #5F00FF +} + +div.highlight .-Color[class*=-BGC57] { + background-color: #5F00FF +} + +div.highlight .-Color[class*=-C58] { + color: #5F5F00 +} + +div.highlight .-Color[class*=-BGC58] { + background-color: #5F5F00 +} + +div.highlight .-Color[class*=-C59] { + color: #5F5F5F +} + +div.highlight .-Color[class*=-BGC59] { + background-color: #5F5F5F +} + +div.highlight .-Color[class*=-C60] { + color: #5F5F87 +} + +div.highlight .-Color[class*=-BGC60] { + background-color: #5F5F87 +} + +div.highlight .-Color[class*=-C61] { + color: #5F5FAF +} + +div.highlight .-Color[class*=-BGC61] { + background-color: #5F5FAF +} + +div.highlight .-Color[class*=-C62] { + color: #5F5FD7 +} + +div.highlight .-Color[class*=-BGC62] { + background-color: #5F5FD7 +} + +div.highlight .-Color[class*=-C63] { + color: #5F5FFF +} + +div.highlight .-Color[class*=-BGC63] { + background-color: #5F5FFF +} + +div.highlight .-Color[class*=-C64] { + color: #5F8700 +} + +div.highlight .-Color[class*=-BGC64] { + background-color: #5F8700 +} + +div.highlight .-Color[class*=-C65] { + color: #5F875F +} + +div.highlight .-Color[class*=-BGC65] { + background-color: #5F875F +} + +div.highlight .-Color[class*=-C66] { + color: #5F8787 +} + +div.highlight .-Color[class*=-BGC66] { + background-color: #5F8787 +} + +div.highlight .-Color[class*=-C67] { + color: #5F87AF +} + +div.highlight .-Color[class*=-BGC67] { + background-color: #5F87AF +} + +div.highlight .-Color[class*=-C68] { + color: #5F87D7 +} + +div.highlight .-Color[class*=-BGC68] { + background-color: #5F87D7 +} + +div.highlight .-Color[class*=-C69] { + color: #5F87FF +} + +div.highlight .-Color[class*=-BGC69] { + background-color: #5F87FF +} + +div.highlight .-Color[class*=-C70] { + color: #5FAF00 +} + +div.highlight .-Color[class*=-BGC70] { + background-color: #5FAF00 +} + +div.highlight .-Color[class*=-C71] { + color: #5FAF5F +} + +div.highlight .-Color[class*=-BGC71] { + background-color: #5FAF5F +} + +div.highlight .-Color[class*=-C72] { + color: #5FAF87 +} + +div.highlight .-Color[class*=-BGC72] { + background-color: #5FAF87 +} + +div.highlight .-Color[class*=-C73] { + color: #5FAFAF +} + +div.highlight .-Color[class*=-BGC73] { + background-color: #5FAFAF +} + +div.highlight .-Color[class*=-C74] { + color: #5FAFD7 +} + +div.highlight .-Color[class*=-BGC74] { + background-color: #5FAFD7 +} + +div.highlight .-Color[class*=-C75] { + color: #5FAFFF +} + +div.highlight .-Color[class*=-BGC75] { + background-color: #5FAFFF +} + +div.highlight .-Color[class*=-C76] { + color: #5FD700 +} + +div.highlight .-Color[class*=-BGC76] { + background-color: #5FD700 +} + +div.highlight .-Color[class*=-C77] { + color: #5FD75F +} + +div.highlight .-Color[class*=-BGC77] { + background-color: #5FD75F +} + +div.highlight .-Color[class*=-C78] { + color: #5FD787 +} + +div.highlight .-Color[class*=-BGC78] { + background-color: #5FD787 +} + +div.highlight .-Color[class*=-C79] { + color: #5FD7AF +} + +div.highlight .-Color[class*=-BGC79] { + background-color: #5FD7AF +} + +div.highlight .-Color[class*=-C80] { + color: #5FD7D7 +} + +div.highlight .-Color[class*=-BGC80] { + background-color: #5FD7D7 +} + +div.highlight .-Color[class*=-C81] { + color: #5FD7FF +} + +div.highlight .-Color[class*=-BGC81] { + background-color: #5FD7FF +} + +div.highlight .-Color[class*=-C82] { + color: #5FFF00 +} + +div.highlight .-Color[class*=-BGC82] { + background-color: #5FFF00 +} + +div.highlight .-Color[class*=-C83] { + color: #5FFF5F +} + +div.highlight .-Color[class*=-BGC83] { + background-color: #5FFF5F +} + +div.highlight .-Color[class*=-C84] { + color: #5FFF87 +} + +div.highlight .-Color[class*=-BGC84] { + background-color: #5FFF87 +} + +div.highlight .-Color[class*=-C85] { + color: #5FFFAF +} + +div.highlight .-Color[class*=-BGC85] { + background-color: #5FFFAF +} + +div.highlight .-Color[class*=-C86] { + color: #5FFFD7 +} + +div.highlight .-Color[class*=-BGC86] { + background-color: #5FFFD7 +} + +div.highlight .-Color[class*=-C87] { + color: #5FFFFF +} + +div.highlight .-Color[class*=-BGC87] { + background-color: #5FFFFF +} + +div.highlight .-Color[class*=-C88] { + color: #870000 +} + +div.highlight .-Color[class*=-BGC88] { + background-color: #870000 +} + +div.highlight .-Color[class*=-C89] { + color: #87005F +} + +div.highlight .-Color[class*=-BGC89] { + background-color: #87005F +} + +div.highlight .-Color[class*=-C90] { + color: #870087 +} + +div.highlight .-Color[class*=-BGC90] { + background-color: #870087 +} + +div.highlight .-Color[class*=-C91] { + color: #8700AF +} + +div.highlight .-Color[class*=-BGC91] { + background-color: #8700AF +} + +div.highlight .-Color[class*=-C92] { + color: #8700D7 +} + +div.highlight .-Color[class*=-BGC92] { + background-color: #8700D7 +} + +div.highlight .-Color[class*=-C93] { + color: #8700FF +} + +div.highlight .-Color[class*=-BGC93] { + background-color: #8700FF +} + +div.highlight .-Color[class*=-C94] { + color: #875F00 +} + +div.highlight .-Color[class*=-BGC94] { + background-color: #875F00 +} + +div.highlight .-Color[class*=-C95] { + color: #875F5F +} + +div.highlight .-Color[class*=-BGC95] { + background-color: #875F5F +} + +div.highlight .-Color[class*=-C96] { + color: #875F87 +} + +div.highlight .-Color[class*=-BGC96] { + background-color: #875F87 +} + +div.highlight .-Color[class*=-C97] { + color: #875FAF +} + +div.highlight .-Color[class*=-BGC97] { + background-color: #875FAF +} + +div.highlight .-Color[class*=-C98] { + color: #875FD7 +} + +div.highlight .-Color[class*=-BGC98] { + background-color: #875FD7 +} + +div.highlight .-Color[class*=-C99] { + color: #875FFF +} + +div.highlight .-Color[class*=-BGC99] { + background-color: #875FFF +} + +div.highlight .-Color[class*=-C100] { + color: #878700 +} + +div.highlight .-Color[class*=-BGC100] { + background-color: #878700 +} + +div.highlight .-Color[class*=-C101] { + color: #87875F +} + +div.highlight .-Color[class*=-BGC101] { + background-color: #87875F +} + +div.highlight .-Color[class*=-C102] { + color: #878787 +} + +div.highlight .-Color[class*=-BGC102] { + background-color: #878787 +} + +div.highlight .-Color[class*=-C103] { + color: #8787AF +} + +div.highlight .-Color[class*=-BGC103] { + background-color: #8787AF +} + +div.highlight .-Color[class*=-C104] { + color: #8787D7 +} + +div.highlight .-Color[class*=-BGC104] { + background-color: #8787D7 +} + +div.highlight .-Color[class*=-C105] { + color: #8787FF +} + +div.highlight .-Color[class*=-BGC105] { + background-color: #8787FF +} + +div.highlight .-Color[class*=-C106] { + color: #87AF00 +} + +div.highlight .-Color[class*=-BGC106] { + background-color: #87AF00 +} + +div.highlight .-Color[class*=-C107] { + color: #87AF5F +} + +div.highlight .-Color[class*=-BGC107] { + background-color: #87AF5F +} + +div.highlight .-Color[class*=-C108] { + color: #87AF87 +} + +div.highlight .-Color[class*=-BGC108] { + background-color: #87AF87 +} + +div.highlight .-Color[class*=-C109] { + color: #87AFAF +} + +div.highlight .-Color[class*=-BGC109] { + background-color: #87AFAF +} + +div.highlight .-Color[class*=-C110] { + color: #87AFD7 +} + +div.highlight .-Color[class*=-BGC110] { + background-color: #87AFD7 +} + +div.highlight .-Color[class*=-C111] { + color: #87AFFF +} + +div.highlight .-Color[class*=-BGC111] { + background-color: #87AFFF +} + +div.highlight .-Color[class*=-C112] { + color: #87D700 +} + +div.highlight .-Color[class*=-BGC112] { + background-color: #87D700 +} + +div.highlight .-Color[class*=-C113] { + color: #87D75F +} + +div.highlight .-Color[class*=-BGC113] { + background-color: #87D75F +} + +div.highlight .-Color[class*=-C114] { + color: #87D787 +} + +div.highlight .-Color[class*=-BGC114] { + background-color: #87D787 +} + +div.highlight .-Color[class*=-C115] { + color: #87D7AF +} + +div.highlight .-Color[class*=-BGC115] { + background-color: #87D7AF +} + +div.highlight .-Color[class*=-C116] { + color: #87D7D7 +} + +div.highlight .-Color[class*=-BGC116] { + background-color: #87D7D7 +} + +div.highlight .-Color[class*=-C117] { + color: #87D7FF +} + +div.highlight .-Color[class*=-BGC117] { + background-color: #87D7FF +} + +div.highlight .-Color[class*=-C118] { + color: #87FF00 +} + +div.highlight .-Color[class*=-BGC118] { + background-color: #87FF00 +} + +div.highlight .-Color[class*=-C119] { + color: #87FF5F +} + +div.highlight .-Color[class*=-BGC119] { + background-color: #87FF5F +} + +div.highlight .-Color[class*=-C120] { + color: #87FF87 +} + +div.highlight .-Color[class*=-BGC120] { + background-color: #87FF87 +} + +div.highlight .-Color[class*=-C121] { + color: #87FFAF +} + +div.highlight .-Color[class*=-BGC121] { + background-color: #87FFAF +} + +div.highlight .-Color[class*=-C122] { + color: #87FFD7 +} + +div.highlight .-Color[class*=-BGC122] { + background-color: #87FFD7 +} + +div.highlight .-Color[class*=-C123] { + color: #87FFFF +} + +div.highlight .-Color[class*=-BGC123] { + background-color: #87FFFF +} + +div.highlight .-Color[class*=-C124] { + color: #AF0000 +} + +div.highlight .-Color[class*=-BGC124] { + background-color: #AF0000 +} + +div.highlight .-Color[class*=-C125] { + color: #AF005F +} + +div.highlight .-Color[class*=-BGC125] { + background-color: #AF005F +} + +div.highlight .-Color[class*=-C126] { + color: #AF0087 +} + +div.highlight .-Color[class*=-BGC126] { + background-color: #AF0087 +} + +div.highlight .-Color[class*=-C127] { + color: #AF00AF +} + +div.highlight .-Color[class*=-BGC127] { + background-color: #AF00AF +} + +div.highlight .-Color[class*=-C128] { + color: #AF00D7 +} + +div.highlight .-Color[class*=-BGC128] { + background-color: #AF00D7 +} + +div.highlight .-Color[class*=-C129] { + color: #AF00FF +} + +div.highlight .-Color[class*=-BGC129] { + background-color: #AF00FF +} + +div.highlight .-Color[class*=-C130] { + color: #AF5F00 +} + +div.highlight .-Color[class*=-BGC130] { + background-color: #AF5F00 +} + +div.highlight .-Color[class*=-C131] { + color: #AF5F5F +} + +div.highlight .-Color[class*=-BGC131] { + background-color: #AF5F5F +} + +div.highlight .-Color[class*=-C132] { + color: #AF5F87 +} + +div.highlight .-Color[class*=-BGC132] { + background-color: #AF5F87 +} + +div.highlight .-Color[class*=-C133] { + color: #AF5FAF +} + +div.highlight .-Color[class*=-BGC133] { + background-color: #AF5FAF +} + +div.highlight .-Color[class*=-C134] { + color: #AF5FD7 +} + +div.highlight .-Color[class*=-BGC134] { + background-color: #AF5FD7 +} + +div.highlight .-Color[class*=-C135] { + color: #AF5FFF +} + +div.highlight .-Color[class*=-BGC135] { + background-color: #AF5FFF +} + +div.highlight .-Color[class*=-C136] { + color: #AF8700 +} + +div.highlight .-Color[class*=-BGC136] { + background-color: #AF8700 +} + +div.highlight .-Color[class*=-C137] { + color: #AF875F +} + +div.highlight .-Color[class*=-BGC137] { + background-color: #AF875F +} + +div.highlight .-Color[class*=-C138] { + color: #AF8787 +} + +div.highlight .-Color[class*=-BGC138] { + background-color: #AF8787 +} + +div.highlight .-Color[class*=-C139] { + color: #AF87AF +} + +div.highlight .-Color[class*=-BGC139] { + background-color: #AF87AF +} + +div.highlight .-Color[class*=-C140] { + color: #AF87D7 +} + +div.highlight .-Color[class*=-BGC140] { + background-color: #AF87D7 +} + +div.highlight .-Color[class*=-C141] { + color: #AF87FF +} + +div.highlight .-Color[class*=-BGC141] { + background-color: #AF87FF +} + +div.highlight .-Color[class*=-C142] { + color: #AFAF00 +} + +div.highlight .-Color[class*=-BGC142] { + background-color: #AFAF00 +} + +div.highlight .-Color[class*=-C143] { + color: #AFAF5F +} + +div.highlight .-Color[class*=-BGC143] { + background-color: #AFAF5F +} + +div.highlight .-Color[class*=-C144] { + color: #AFAF87 +} + +div.highlight .-Color[class*=-BGC144] { + background-color: #AFAF87 +} + +div.highlight .-Color[class*=-C145] { + color: #AFAFAF +} + +div.highlight .-Color[class*=-BGC145] { + background-color: #AFAFAF +} + +div.highlight .-Color[class*=-C146] { + color: #AFAFD7 +} + +div.highlight .-Color[class*=-BGC146] { + background-color: #AFAFD7 +} + +div.highlight .-Color[class*=-C147] { + color: #AFAFFF +} + +div.highlight .-Color[class*=-BGC147] { + background-color: #AFAFFF +} + +div.highlight .-Color[class*=-C148] { + color: #AFD700 +} + +div.highlight .-Color[class*=-BGC148] { + background-color: #AFD700 +} + +div.highlight .-Color[class*=-C149] { + color: #AFD75F +} + +div.highlight .-Color[class*=-BGC149] { + background-color: #AFD75F +} + +div.highlight .-Color[class*=-C150] { + color: #AFD787 +} + +div.highlight .-Color[class*=-BGC150] { + background-color: #AFD787 +} + +div.highlight .-Color[class*=-C151] { + color: #AFD7AF +} + +div.highlight .-Color[class*=-BGC151] { + background-color: #AFD7AF +} + +div.highlight .-Color[class*=-C152] { + color: #AFD7D7 +} + +div.highlight .-Color[class*=-BGC152] { + background-color: #AFD7D7 +} + +div.highlight .-Color[class*=-C153] { + color: #AFD7FF +} + +div.highlight .-Color[class*=-BGC153] { + background-color: #AFD7FF +} + +div.highlight .-Color[class*=-C154] { + color: #AFFF00 +} + +div.highlight .-Color[class*=-BGC154] { + background-color: #AFFF00 +} + +div.highlight .-Color[class*=-C155] { + color: #AFFF5F +} + +div.highlight .-Color[class*=-BGC155] { + background-color: #AFFF5F +} + +div.highlight .-Color[class*=-C156] { + color: #AFFF87 +} + +div.highlight .-Color[class*=-BGC156] { + background-color: #AFFF87 +} + +div.highlight .-Color[class*=-C157] { + color: #AFFFAF +} + +div.highlight .-Color[class*=-BGC157] { + background-color: #AFFFAF +} + +div.highlight .-Color[class*=-C158] { + color: #AFFFD7 +} + +div.highlight .-Color[class*=-BGC158] { + background-color: #AFFFD7 +} + +div.highlight .-Color[class*=-C159] { + color: #AFFFFF +} + +div.highlight .-Color[class*=-BGC159] { + background-color: #AFFFFF +} + +div.highlight .-Color[class*=-C160] { + color: #D70000 +} + +div.highlight .-Color[class*=-BGC160] { + background-color: #D70000 +} + +div.highlight .-Color[class*=-C161] { + color: #D7005F +} + +div.highlight .-Color[class*=-BGC161] { + background-color: #D7005F +} + +div.highlight .-Color[class*=-C162] { + color: #D70087 +} + +div.highlight .-Color[class*=-BGC162] { + background-color: #D70087 +} + +div.highlight .-Color[class*=-C163] { + color: #D700AF +} + +div.highlight .-Color[class*=-BGC163] { + background-color: #D700AF +} + +div.highlight .-Color[class*=-C164] { + color: #D700D7 +} + +div.highlight .-Color[class*=-BGC164] { + background-color: #D700D7 +} + +div.highlight .-Color[class*=-C165] { + color: #D700FF +} + +div.highlight .-Color[class*=-BGC165] { + background-color: #D700FF +} + +div.highlight .-Color[class*=-C166] { + color: #D75F00 +} + +div.highlight .-Color[class*=-BGC166] { + background-color: #D75F00 +} + +div.highlight .-Color[class*=-C167] { + color: #D75F5F +} + +div.highlight .-Color[class*=-BGC167] { + background-color: #D75F5F +} + +div.highlight .-Color[class*=-C168] { + color: #D75F87 +} + +div.highlight .-Color[class*=-BGC168] { + background-color: #D75F87 +} + +div.highlight .-Color[class*=-C169] { + color: #D75FAF +} + +div.highlight .-Color[class*=-BGC169] { + background-color: #D75FAF +} + +div.highlight .-Color[class*=-C170] { + color: #D75FD7 +} + +div.highlight .-Color[class*=-BGC170] { + background-color: #D75FD7 +} + +div.highlight .-Color[class*=-C171] { + color: #D75FFF +} + +div.highlight .-Color[class*=-BGC171] { + background-color: #D75FFF +} + +div.highlight .-Color[class*=-C172] { + color: #D78700 +} + +div.highlight .-Color[class*=-BGC172] { + background-color: #D78700 +} + +div.highlight .-Color[class*=-C173] { + color: #D7875F +} + +div.highlight .-Color[class*=-BGC173] { + background-color: #D7875F +} + +div.highlight .-Color[class*=-C174] { + color: #D78787 +} + +div.highlight .-Color[class*=-BGC174] { + background-color: #D78787 +} + +div.highlight .-Color[class*=-C175] { + color: #D787AF +} + +div.highlight .-Color[class*=-BGC175] { + background-color: #D787AF +} + +div.highlight .-Color[class*=-C176] { + color: #D787D7 +} + +div.highlight .-Color[class*=-BGC176] { + background-color: #D787D7 +} + +div.highlight .-Color[class*=-C177] { + color: #D787FF +} + +div.highlight .-Color[class*=-BGC177] { + background-color: #D787FF +} + +div.highlight .-Color[class*=-C178] { + color: #D7AF00 +} + +div.highlight .-Color[class*=-BGC178] { + background-color: #D7AF00 +} + +div.highlight .-Color[class*=-C179] { + color: #D7AF5F +} + +div.highlight .-Color[class*=-BGC179] { + background-color: #D7AF5F +} + +div.highlight .-Color[class*=-C180] { + color: #D7AF87 +} + +div.highlight .-Color[class*=-BGC180] { + background-color: #D7AF87 +} + +div.highlight .-Color[class*=-C181] { + color: #D7AFAF +} + +div.highlight .-Color[class*=-BGC181] { + background-color: #D7AFAF +} + +div.highlight .-Color[class*=-C182] { + color: #D7AFD7 +} + +div.highlight .-Color[class*=-BGC182] { + background-color: #D7AFD7 +} + +div.highlight .-Color[class*=-C183] { + color: #D7AFFF +} + +div.highlight .-Color[class*=-BGC183] { + background-color: #D7AFFF +} + +div.highlight .-Color[class*=-C184] { + color: #D7D700 +} + +div.highlight .-Color[class*=-BGC184] { + background-color: #D7D700 +} + +div.highlight .-Color[class*=-C185] { + color: #D7D75F +} + +div.highlight .-Color[class*=-BGC185] { + background-color: #D7D75F +} + +div.highlight .-Color[class*=-C186] { + color: #D7D787 +} + +div.highlight .-Color[class*=-BGC186] { + background-color: #D7D787 +} + +div.highlight .-Color[class*=-C187] { + color: #D7D7AF +} + +div.highlight .-Color[class*=-BGC187] { + background-color: #D7D7AF +} + +div.highlight .-Color[class*=-C188] { + color: #D7D7D7 +} + +div.highlight .-Color[class*=-BGC188] { + background-color: #D7D7D7 +} + +div.highlight .-Color[class*=-C189] { + color: #D7D7FF +} + +div.highlight .-Color[class*=-BGC189] { + background-color: #D7D7FF +} + +div.highlight .-Color[class*=-C190] { + color: #D7FF00 +} + +div.highlight .-Color[class*=-BGC190] { + background-color: #D7FF00 +} + +div.highlight .-Color[class*=-C191] { + color: #D7FF5F +} + +div.highlight .-Color[class*=-BGC191] { + background-color: #D7FF5F +} + +div.highlight .-Color[class*=-C192] { + color: #D7FF87 +} + +div.highlight .-Color[class*=-BGC192] { + background-color: #D7FF87 +} + +div.highlight .-Color[class*=-C193] { + color: #D7FFAF +} + +div.highlight .-Color[class*=-BGC193] { + background-color: #D7FFAF +} + +div.highlight .-Color[class*=-C194] { + color: #D7FFD7 +} + +div.highlight .-Color[class*=-BGC194] { + background-color: #D7FFD7 +} + +div.highlight .-Color[class*=-C195] { + color: #D7FFFF +} + +div.highlight .-Color[class*=-BGC195] { + background-color: #D7FFFF +} + +div.highlight .-Color[class*=-C196] { + color: #FF0000 +} + +div.highlight .-Color[class*=-BGC196] { + background-color: #FF0000 +} + +div.highlight .-Color[class*=-C197] { + color: #FF005F +} + +div.highlight .-Color[class*=-BGC197] { + background-color: #FF005F +} + +div.highlight .-Color[class*=-C198] { + color: #FF0087 +} + +div.highlight .-Color[class*=-BGC198] { + background-color: #FF0087 +} + +div.highlight .-Color[class*=-C199] { + color: #FF00AF +} + +div.highlight .-Color[class*=-BGC199] { + background-color: #FF00AF +} + +div.highlight .-Color[class*=-C200] { + color: #FF00D7 +} + +div.highlight .-Color[class*=-BGC200] { + background-color: #FF00D7 +} + +div.highlight .-Color[class*=-C201] { + color: #FF00FF +} + +div.highlight .-Color[class*=-BGC201] { + background-color: #FF00FF +} + +div.highlight .-Color[class*=-C202] { + color: #FF5F00 +} + +div.highlight .-Color[class*=-BGC202] { + background-color: #FF5F00 +} + +div.highlight .-Color[class*=-C203] { + color: #FF5F5F +} + +div.highlight .-Color[class*=-BGC203] { + background-color: #FF5F5F +} + +div.highlight .-Color[class*=-C204] { + color: #FF5F87 +} + +div.highlight .-Color[class*=-BGC204] { + background-color: #FF5F87 +} + +div.highlight .-Color[class*=-C205] { + color: #FF5FAF +} + +div.highlight .-Color[class*=-BGC205] { + background-color: #FF5FAF +} + +div.highlight .-Color[class*=-C206] { + color: #FF5FD7 +} + +div.highlight .-Color[class*=-BGC206] { + background-color: #FF5FD7 +} + +div.highlight .-Color[class*=-C207] { + color: #FF5FFF +} + +div.highlight .-Color[class*=-BGC207] { + background-color: #FF5FFF +} + +div.highlight .-Color[class*=-C208] { + color: #FF8700 +} + +div.highlight .-Color[class*=-BGC208] { + background-color: #FF8700 +} + +div.highlight .-Color[class*=-C209] { + color: #FF875F +} + +div.highlight .-Color[class*=-BGC209] { + background-color: #FF875F +} + +div.highlight .-Color[class*=-C210] { + color: #FF8787 +} + +div.highlight .-Color[class*=-BGC210] { + background-color: #FF8787 +} + +div.highlight .-Color[class*=-C211] { + color: #FF87AF +} + +div.highlight .-Color[class*=-BGC211] { + background-color: #FF87AF +} + +div.highlight .-Color[class*=-C212] { + color: #FF87D7 +} + +div.highlight .-Color[class*=-BGC212] { + background-color: #FF87D7 +} + +div.highlight .-Color[class*=-C213] { + color: #FF87FF +} + +div.highlight .-Color[class*=-BGC213] { + background-color: #FF87FF +} + +div.highlight .-Color[class*=-C214] { + color: #FFAF00 +} + +div.highlight .-Color[class*=-BGC214] { + background-color: #FFAF00 +} + +div.highlight .-Color[class*=-C215] { + color: #FFAF5F +} + +div.highlight .-Color[class*=-BGC215] { + background-color: #FFAF5F +} + +div.highlight .-Color[class*=-C216] { + color: #FFAF87 +} + +div.highlight .-Color[class*=-BGC216] { + background-color: #FFAF87 +} + +div.highlight .-Color[class*=-C217] { + color: #FFAFAF +} + +div.highlight .-Color[class*=-BGC217] { + background-color: #FFAFAF +} + +div.highlight .-Color[class*=-C218] { + color: #FFAFD7 +} + +div.highlight .-Color[class*=-BGC218] { + background-color: #FFAFD7 +} + +div.highlight .-Color[class*=-C219] { + color: #FFAFFF +} + +div.highlight .-Color[class*=-BGC219] { + background-color: #FFAFFF +} + +div.highlight .-Color[class*=-C220] { + color: #FFD700 +} + +div.highlight .-Color[class*=-BGC220] { + background-color: #FFD700 +} + +div.highlight .-Color[class*=-C221] { + color: #FFD75F +} + +div.highlight .-Color[class*=-BGC221] { + background-color: #FFD75F +} + +div.highlight .-Color[class*=-C222] { + color: #FFD787 +} + +div.highlight .-Color[class*=-BGC222] { + background-color: #FFD787 +} + +div.highlight .-Color[class*=-C223] { + color: #FFD7AF +} + +div.highlight .-Color[class*=-BGC223] { + background-color: #FFD7AF +} + +div.highlight .-Color[class*=-C224] { + color: #FFD7D7 +} + +div.highlight .-Color[class*=-BGC224] { + background-color: #FFD7D7 +} + +div.highlight .-Color[class*=-C225] { + color: #FFD7FF +} + +div.highlight .-Color[class*=-BGC225] { + background-color: #FFD7FF +} + +div.highlight .-Color[class*=-C226] { + color: #FFFF00 +} + +div.highlight .-Color[class*=-BGC226] { + background-color: #FFFF00 +} + +div.highlight .-Color[class*=-C227] { + color: #FFFF5F +} + +div.highlight .-Color[class*=-BGC227] { + background-color: #FFFF5F +} + +div.highlight .-Color[class*=-C228] { + color: #FFFF87 +} + +div.highlight .-Color[class*=-BGC228] { + background-color: #FFFF87 +} + +div.highlight .-Color[class*=-C229] { + color: #FFFFAF +} + +div.highlight .-Color[class*=-BGC229] { + background-color: #FFFFAF +} + +div.highlight .-Color[class*=-C230] { + color: #FFFFD7 +} + +div.highlight .-Color[class*=-BGC230] { + background-color: #FFFFD7 +} + +div.highlight .-Color[class*=-C231] { + color: #FFFFFF +} + +div.highlight .-Color[class*=-BGC231] { + background-color: #FFFFFF +} + +div.highlight .-Color[class*=-C232] { + color: #080808 +} + +div.highlight .-Color[class*=-BGC232] { + background-color: #080808 +} + +div.highlight .-Color[class*=-C233] { + color: #121212 +} + +div.highlight .-Color[class*=-BGC233] { + background-color: #121212 +} + +div.highlight .-Color[class*=-C234] { + color: #1C1C1C +} + +div.highlight .-Color[class*=-BGC234] { + background-color: #1C1C1C +} + +div.highlight .-Color[class*=-C235] { + color: #262626 +} + +div.highlight .-Color[class*=-BGC235] { + background-color: #262626 +} + +div.highlight .-Color[class*=-C236] { + color: #303030 +} + +div.highlight .-Color[class*=-BGC236] { + background-color: #303030 +} + +div.highlight .-Color[class*=-C237] { + color: #3A3A3A +} + +div.highlight .-Color[class*=-BGC237] { + background-color: #3A3A3A +} + +div.highlight .-Color[class*=-C238] { + color: #444444 +} + +div.highlight .-Color[class*=-BGC238] { + background-color: #444444 +} + +div.highlight .-Color[class*=-C239] { + color: #4E4E4E +} + +div.highlight .-Color[class*=-BGC239] { + background-color: #4E4E4E +} + +div.highlight .-Color[class*=-C240] { + color: #585858 +} + +div.highlight .-Color[class*=-BGC240] { + background-color: #585858 +} + +div.highlight .-Color[class*=-C241] { + color: #626262 +} + +div.highlight .-Color[class*=-BGC241] { + background-color: #626262 +} + +div.highlight .-Color[class*=-C242] { + color: #6C6C6C +} + +div.highlight .-Color[class*=-BGC242] { + background-color: #6C6C6C +} + +div.highlight .-Color[class*=-C243] { + color: #767676 +} + +div.highlight .-Color[class*=-BGC243] { + background-color: #767676 +} + +div.highlight .-Color[class*=-C244] { + color: #808080 +} + +div.highlight .-Color[class*=-BGC244] { + background-color: #808080 +} + +div.highlight .-Color[class*=-C245] { + color: #8A8A8A +} + +div.highlight .-Color[class*=-BGC245] { + background-color: #8A8A8A +} + +div.highlight .-Color[class*=-C246] { + color: #949494 +} + +div.highlight .-Color[class*=-BGC246] { + background-color: #949494 +} + +div.highlight .-Color[class*=-C247] { + color: #9E9E9E +} + +div.highlight .-Color[class*=-BGC247] { + background-color: #9E9E9E +} + +div.highlight .-Color[class*=-C248] { + color: #A8A8A8 +} + +div.highlight .-Color[class*=-BGC248] { + background-color: #A8A8A8 +} + +div.highlight .-Color[class*=-C249] { + color: #B2B2B2 +} + +div.highlight .-Color[class*=-BGC249] { + background-color: #B2B2B2 +} + +div.highlight .-Color[class*=-C250] { + color: #BCBCBC +} + +div.highlight .-Color[class*=-BGC250] { + background-color: #BCBCBC +} + +div.highlight .-Color[class*=-C251] { + color: #C6C6C6 +} + +div.highlight .-Color[class*=-BGC251] { + background-color: #C6C6C6 +} + +div.highlight .-Color[class*=-C252] { + color: #D0D0D0 +} + +div.highlight .-Color[class*=-BGC252] { + background-color: #D0D0D0 +} + +div.highlight .-Color[class*=-C253] { + color: #DADADA +} + +div.highlight .-Color[class*=-BGC253] { + background-color: #DADADA +} + +div.highlight .-Color[class*=-C254] { + color: #E4E4E4 +} + +div.highlight .-Color[class*=-BGC254] { + background-color: #E4E4E4 +} + +div.highlight .-Color[class*=-C255] { + color: #EEEEEE +} + +div.highlight .-Color[class*=-BGC255] { + background-color: #EEEEEE +} diff --git a/_static/play-solid.svg b/_static/play-solid.svg new file mode 100644 index 00000000..bcd81f7a --- /dev/null +++ b/_static/play-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/_static/plus.png b/_static/plus.png new file mode 100644 index 00000000..7107cec9 Binary files /dev/null and b/_static/plus.png differ diff --git a/_static/pygments.css b/_static/pygments.css new file mode 100644 index 00000000..997797f2 --- /dev/null +++ b/_static/pygments.css @@ -0,0 +1,152 @@ +html[data-theme="light"] .highlight pre { line-height: 125%; } +html[data-theme="light"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight .hll { background-color: #7971292e } +html[data-theme="light"] .highlight { background: #fefefe; color: #545454 } +html[data-theme="light"] .highlight .c { color: #797129 } /* Comment */ +html[data-theme="light"] .highlight .err { color: #d91e18 } /* Error */ +html[data-theme="light"] .highlight .k { color: #7928a1 } /* Keyword */ +html[data-theme="light"] .highlight .l { color: #797129 } /* Literal */ +html[data-theme="light"] .highlight .n { color: #545454 } /* Name */ +html[data-theme="light"] .highlight .o { color: #008000 } /* Operator */ +html[data-theme="light"] .highlight .p { color: #545454 } /* Punctuation */ +html[data-theme="light"] .highlight .ch { color: #797129 } /* Comment.Hashbang */ +html[data-theme="light"] .highlight .cm { color: #797129 } /* Comment.Multiline */ +html[data-theme="light"] .highlight .cp { color: #797129 } /* Comment.Preproc */ +html[data-theme="light"] .highlight .cpf { color: #797129 } /* Comment.PreprocFile */ +html[data-theme="light"] .highlight .c1 { color: #797129 } /* Comment.Single */ +html[data-theme="light"] .highlight .cs { color: #797129 } /* Comment.Special */ +html[data-theme="light"] .highlight .gd { color: #007faa } /* Generic.Deleted */ +html[data-theme="light"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="light"] .highlight .gh { color: #007faa } /* Generic.Heading */ +html[data-theme="light"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="light"] .highlight .gu { color: #007faa } /* Generic.Subheading */ +html[data-theme="light"] .highlight .kc { color: #7928a1 } /* Keyword.Constant */ +html[data-theme="light"] .highlight .kd { color: #7928a1 } /* Keyword.Declaration */ +html[data-theme="light"] .highlight .kn { color: #7928a1 } /* Keyword.Namespace */ +html[data-theme="light"] .highlight .kp { color: #7928a1 } /* Keyword.Pseudo */ +html[data-theme="light"] .highlight .kr { color: #7928a1 } /* Keyword.Reserved */ +html[data-theme="light"] .highlight .kt { color: #797129 } /* Keyword.Type */ +html[data-theme="light"] .highlight .ld { color: #797129 } /* Literal.Date */ +html[data-theme="light"] .highlight .m { color: #797129 } /* Literal.Number */ +html[data-theme="light"] .highlight .s { color: #008000 } /* Literal.String */ +html[data-theme="light"] .highlight .na { color: #797129 } /* Name.Attribute */ +html[data-theme="light"] .highlight .nb { color: #797129 } /* Name.Builtin */ +html[data-theme="light"] .highlight .nc { color: #007faa } /* Name.Class */ +html[data-theme="light"] .highlight .no { color: #007faa } /* Name.Constant */ +html[data-theme="light"] .highlight .nd { color: #797129 } /* Name.Decorator */ +html[data-theme="light"] .highlight .ni { color: #008000 } /* Name.Entity */ +html[data-theme="light"] .highlight .ne { color: #7928a1 } /* Name.Exception */ +html[data-theme="light"] .highlight .nf { color: #007faa } /* Name.Function */ +html[data-theme="light"] .highlight .nl { color: #797129 } /* Name.Label */ +html[data-theme="light"] .highlight .nn { color: #545454 } /* Name.Namespace */ +html[data-theme="light"] .highlight .nx { color: #545454 } /* Name.Other */ +html[data-theme="light"] .highlight .py { color: #007faa } /* Name.Property */ +html[data-theme="light"] .highlight .nt { color: #007faa } /* Name.Tag */ +html[data-theme="light"] .highlight .nv { color: #d91e18 } /* Name.Variable */ +html[data-theme="light"] .highlight .ow { color: #7928a1 } /* Operator.Word */ +html[data-theme="light"] .highlight .pm { color: #545454 } /* Punctuation.Marker */ +html[data-theme="light"] .highlight .w { color: #545454 } /* Text.Whitespace */ +html[data-theme="light"] .highlight .mb { color: #797129 } /* Literal.Number.Bin */ +html[data-theme="light"] .highlight .mf { color: #797129 } /* Literal.Number.Float */ +html[data-theme="light"] .highlight .mh { color: #797129 } /* Literal.Number.Hex */ +html[data-theme="light"] .highlight .mi { color: #797129 } /* Literal.Number.Integer */ +html[data-theme="light"] .highlight .mo { color: #797129 } /* Literal.Number.Oct */ +html[data-theme="light"] .highlight .sa { color: #008000 } /* Literal.String.Affix */ +html[data-theme="light"] .highlight .sb { color: #008000 } /* Literal.String.Backtick */ +html[data-theme="light"] .highlight .sc { color: #008000 } /* Literal.String.Char */ +html[data-theme="light"] .highlight .dl { color: #008000 } /* Literal.String.Delimiter */ +html[data-theme="light"] .highlight .sd { color: #008000 } /* Literal.String.Doc */ +html[data-theme="light"] .highlight .s2 { color: #008000 } /* Literal.String.Double */ +html[data-theme="light"] .highlight .se { color: #008000 } /* Literal.String.Escape */ +html[data-theme="light"] .highlight .sh { color: #008000 } /* Literal.String.Heredoc */ +html[data-theme="light"] .highlight .si { color: #008000 } /* Literal.String.Interpol */ +html[data-theme="light"] .highlight .sx { color: #008000 } /* Literal.String.Other */ +html[data-theme="light"] .highlight .sr { color: #d91e18 } /* Literal.String.Regex */ +html[data-theme="light"] .highlight .s1 { color: #008000 } /* Literal.String.Single */ +html[data-theme="light"] .highlight .ss { color: #007faa } /* Literal.String.Symbol */ +html[data-theme="light"] .highlight .bp { color: #797129 } /* Name.Builtin.Pseudo */ +html[data-theme="light"] .highlight .fm { color: #007faa } /* Name.Function.Magic */ +html[data-theme="light"] .highlight .vc { color: #d91e18 } /* Name.Variable.Class */ +html[data-theme="light"] .highlight .vg { color: #d91e18 } /* Name.Variable.Global */ +html[data-theme="light"] .highlight .vi { color: #d91e18 } /* Name.Variable.Instance */ +html[data-theme="light"] .highlight .vm { color: #797129 } /* Name.Variable.Magic */ +html[data-theme="light"] .highlight .il { color: #797129 } /* Literal.Number.Integer.Long */ +html[data-theme="dark"] .highlight pre { line-height: 125%; } +html[data-theme="dark"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight .hll { background-color: #ffd9002e } +html[data-theme="dark"] .highlight { background: #2b2b2b; color: #f8f8f2 } +html[data-theme="dark"] .highlight .c { color: #ffd900 } /* Comment */ +html[data-theme="dark"] .highlight .err { color: #ffa07a } /* Error */ +html[data-theme="dark"] .highlight .k { color: #dcc6e0 } /* Keyword */ +html[data-theme="dark"] .highlight .l { color: #ffd900 } /* Literal */ +html[data-theme="dark"] .highlight .n { color: #f8f8f2 } /* Name */ +html[data-theme="dark"] .highlight .o { color: #abe338 } /* Operator */ +html[data-theme="dark"] .highlight .p { color: #f8f8f2 } /* Punctuation */ +html[data-theme="dark"] .highlight .ch { color: #ffd900 } /* Comment.Hashbang */ +html[data-theme="dark"] .highlight .cm { color: #ffd900 } /* Comment.Multiline */ +html[data-theme="dark"] .highlight .cp { color: #ffd900 } /* Comment.Preproc */ +html[data-theme="dark"] .highlight .cpf { color: #ffd900 } /* Comment.PreprocFile */ +html[data-theme="dark"] .highlight .c1 { color: #ffd900 } /* Comment.Single */ +html[data-theme="dark"] .highlight .cs { color: #ffd900 } /* Comment.Special */ +html[data-theme="dark"] .highlight .gd { color: #00e0e0 } /* Generic.Deleted */ +html[data-theme="dark"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="dark"] .highlight .gh { color: #00e0e0 } /* Generic.Heading */ +html[data-theme="dark"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="dark"] .highlight .gu { color: #00e0e0 } /* Generic.Subheading */ +html[data-theme="dark"] .highlight .kc { color: #dcc6e0 } /* Keyword.Constant */ +html[data-theme="dark"] .highlight .kd { color: #dcc6e0 } /* Keyword.Declaration */ +html[data-theme="dark"] .highlight .kn { color: #dcc6e0 } /* Keyword.Namespace */ +html[data-theme="dark"] .highlight .kp { color: #dcc6e0 } /* Keyword.Pseudo */ +html[data-theme="dark"] .highlight .kr { color: #dcc6e0 } /* Keyword.Reserved */ +html[data-theme="dark"] .highlight .kt { color: #ffd900 } /* Keyword.Type */ +html[data-theme="dark"] .highlight .ld { color: #ffd900 } /* Literal.Date */ +html[data-theme="dark"] .highlight .m { color: #ffd900 } /* Literal.Number */ +html[data-theme="dark"] .highlight .s { color: #abe338 } /* Literal.String */ +html[data-theme="dark"] .highlight .na { color: #ffd900 } /* Name.Attribute */ +html[data-theme="dark"] .highlight .nb { color: #ffd900 } /* Name.Builtin */ +html[data-theme="dark"] .highlight .nc { color: #00e0e0 } /* Name.Class */ +html[data-theme="dark"] .highlight .no { color: #00e0e0 } /* Name.Constant */ +html[data-theme="dark"] .highlight .nd { color: #ffd900 } /* Name.Decorator */ +html[data-theme="dark"] .highlight .ni { color: #abe338 } /* Name.Entity */ +html[data-theme="dark"] .highlight .ne { color: #dcc6e0 } /* Name.Exception */ +html[data-theme="dark"] .highlight .nf { color: #00e0e0 } /* Name.Function */ +html[data-theme="dark"] .highlight .nl { color: #ffd900 } /* Name.Label */ +html[data-theme="dark"] .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ +html[data-theme="dark"] .highlight .nx { color: #f8f8f2 } /* Name.Other */ +html[data-theme="dark"] .highlight .py { color: #00e0e0 } /* Name.Property */ +html[data-theme="dark"] .highlight .nt { color: #00e0e0 } /* Name.Tag */ +html[data-theme="dark"] .highlight .nv { color: #ffa07a } /* Name.Variable */ +html[data-theme="dark"] .highlight .ow { color: #dcc6e0 } /* Operator.Word */ +html[data-theme="dark"] .highlight .pm { color: #f8f8f2 } /* Punctuation.Marker */ +html[data-theme="dark"] .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ +html[data-theme="dark"] .highlight .mb { color: #ffd900 } /* Literal.Number.Bin */ +html[data-theme="dark"] .highlight .mf { color: #ffd900 } /* Literal.Number.Float */ +html[data-theme="dark"] .highlight .mh { color: #ffd900 } /* Literal.Number.Hex */ +html[data-theme="dark"] .highlight .mi { color: #ffd900 } /* Literal.Number.Integer */ +html[data-theme="dark"] .highlight .mo { color: #ffd900 } /* Literal.Number.Oct */ +html[data-theme="dark"] .highlight .sa { color: #abe338 } /* Literal.String.Affix */ +html[data-theme="dark"] .highlight .sb { color: #abe338 } /* Literal.String.Backtick */ +html[data-theme="dark"] .highlight .sc { color: #abe338 } /* Literal.String.Char */ +html[data-theme="dark"] .highlight .dl { color: #abe338 } /* Literal.String.Delimiter */ +html[data-theme="dark"] .highlight .sd { color: #abe338 } /* Literal.String.Doc */ +html[data-theme="dark"] .highlight .s2 { color: #abe338 } /* Literal.String.Double */ +html[data-theme="dark"] .highlight .se { color: #abe338 } /* Literal.String.Escape */ +html[data-theme="dark"] .highlight .sh { color: #abe338 } /* Literal.String.Heredoc */ +html[data-theme="dark"] .highlight .si { color: #abe338 } /* Literal.String.Interpol */ +html[data-theme="dark"] .highlight .sx { color: #abe338 } /* Literal.String.Other */ +html[data-theme="dark"] .highlight .sr { color: #ffa07a } /* Literal.String.Regex */ +html[data-theme="dark"] .highlight .s1 { color: #abe338 } /* Literal.String.Single */ +html[data-theme="dark"] .highlight .ss { color: #00e0e0 } /* Literal.String.Symbol */ +html[data-theme="dark"] .highlight .bp { color: #ffd900 } /* Name.Builtin.Pseudo */ +html[data-theme="dark"] .highlight .fm { color: #00e0e0 } /* Name.Function.Magic */ +html[data-theme="dark"] .highlight .vc { color: #ffa07a } /* Name.Variable.Class */ +html[data-theme="dark"] .highlight .vg { color: #ffa07a } /* Name.Variable.Global */ +html[data-theme="dark"] .highlight .vi { color: #ffa07a } /* Name.Variable.Instance */ +html[data-theme="dark"] .highlight .vm { color: #ffd900 } /* Name.Variable.Magic */ +html[data-theme="dark"] .highlight .il { color: #ffd900 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/_static/sbt-webpack-macros.html b/_static/sbt-webpack-macros.html new file mode 100644 index 00000000..6cbf559f --- /dev/null +++ b/_static/sbt-webpack-macros.html @@ -0,0 +1,11 @@ + +{% macro head_pre_bootstrap() %} + +{% endmacro %} + +{% macro body_post() %} + +{% endmacro %} diff --git a/_static/scripts/bootstrap.js b/_static/scripts/bootstrap.js new file mode 100644 index 00000000..4e209b0e --- /dev/null +++ b/_static/scripts/bootstrap.js @@ -0,0 +1,3 @@ +/*! For license information please see bootstrap.js.LICENSE.txt */ +(()=>{"use strict";var t={d:(e,i)=>{for(var n in i)t.o(i,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:i[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};t.r(e),t.d(e,{afterMain:()=>E,afterRead:()=>v,afterWrite:()=>C,applyStyles:()=>$,arrow:()=>J,auto:()=>a,basePlacements:()=>l,beforeMain:()=>y,beforeRead:()=>_,beforeWrite:()=>A,bottom:()=>s,clippingParents:()=>d,computeStyles:()=>it,createPopper:()=>Dt,createPopperBase:()=>St,createPopperLite:()=>$t,detectOverflow:()=>_t,end:()=>h,eventListeners:()=>st,flip:()=>bt,hide:()=>wt,left:()=>r,main:()=>w,modifierPhases:()=>O,offset:()=>Et,placements:()=>g,popper:()=>f,popperGenerator:()=>Lt,popperOffsets:()=>At,preventOverflow:()=>Tt,read:()=>b,reference:()=>p,right:()=>o,start:()=>c,top:()=>n,variationPlacements:()=>m,viewport:()=>u,write:()=>T});var i={};t.r(i),t.d(i,{Alert:()=>Oe,Button:()=>ke,Carousel:()=>ri,Collapse:()=>yi,Dropdown:()=>Vi,Modal:()=>xn,Offcanvas:()=>Vn,Popover:()=>fs,ScrollSpy:()=>Ts,Tab:()=>Ks,Toast:()=>lo,Tooltip:()=>hs});var n="top",s="bottom",o="right",r="left",a="auto",l=[n,s,o,r],c="start",h="end",d="clippingParents",u="viewport",f="popper",p="reference",m=l.reduce((function(t,e){return t.concat([e+"-"+c,e+"-"+h])}),[]),g=[].concat(l,[a]).reduce((function(t,e){return t.concat([e,e+"-"+c,e+"-"+h])}),[]),_="beforeRead",b="read",v="afterRead",y="beforeMain",w="main",E="afterMain",A="beforeWrite",T="write",C="afterWrite",O=[_,b,v,y,w,E,A,T,C];function x(t){return t?(t.nodeName||"").toLowerCase():null}function k(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function L(t){return t instanceof k(t).Element||t instanceof Element}function S(t){return t instanceof k(t).HTMLElement||t instanceof HTMLElement}function D(t){return"undefined"!=typeof ShadowRoot&&(t instanceof k(t).ShadowRoot||t instanceof ShadowRoot)}const $={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];S(s)&&x(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});S(n)&&x(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function I(t){return t.split("-")[0]}var N=Math.max,P=Math.min,M=Math.round;function j(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function F(){return!/^((?!chrome|android).)*safari/i.test(j())}function H(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&S(t)&&(s=t.offsetWidth>0&&M(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&M(n.height)/t.offsetHeight||1);var r=(L(t)?k(t):window).visualViewport,a=!F()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function B(t){var e=H(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function W(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&D(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function z(t){return k(t).getComputedStyle(t)}function R(t){return["table","td","th"].indexOf(x(t))>=0}function q(t){return((L(t)?t.ownerDocument:t.document)||window.document).documentElement}function V(t){return"html"===x(t)?t:t.assignedSlot||t.parentNode||(D(t)?t.host:null)||q(t)}function Y(t){return S(t)&&"fixed"!==z(t).position?t.offsetParent:null}function K(t){for(var e=k(t),i=Y(t);i&&R(i)&&"static"===z(i).position;)i=Y(i);return i&&("html"===x(i)||"body"===x(i)&&"static"===z(i).position)?e:i||function(t){var e=/firefox/i.test(j());if(/Trident/i.test(j())&&S(t)&&"fixed"===z(t).position)return null;var i=V(t);for(D(i)&&(i=i.host);S(i)&&["html","body"].indexOf(x(i))<0;){var n=z(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Q(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function X(t,e,i){return N(t,P(e,i))}function U(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function G(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const J={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,a=t.name,c=t.options,h=i.elements.arrow,d=i.modifiersData.popperOffsets,u=I(i.placement),f=Q(u),p=[r,o].indexOf(u)>=0?"height":"width";if(h&&d){var m=function(t,e){return U("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:G(t,l))}(c.padding,i),g=B(h),_="y"===f?n:r,b="y"===f?s:o,v=i.rects.reference[p]+i.rects.reference[f]-d[f]-i.rects.popper[p],y=d[f]-i.rects.reference[f],w=K(h),E=w?"y"===f?w.clientHeight||0:w.clientWidth||0:0,A=v/2-y/2,T=m[_],C=E-g[p]-m[b],O=E/2-g[p]/2+A,x=X(T,O,C),k=f;i.modifiersData[a]=((e={})[k]=x,e.centerOffset=x-O,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&W(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Z(t){return t.split("-")[1]}var tt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function et(t){var e,i=t.popper,a=t.popperRect,l=t.placement,c=t.variation,d=t.offsets,u=t.position,f=t.gpuAcceleration,p=t.adaptive,m=t.roundOffsets,g=t.isFixed,_=d.x,b=void 0===_?0:_,v=d.y,y=void 0===v?0:v,w="function"==typeof m?m({x:b,y}):{x:b,y};b=w.x,y=w.y;var E=d.hasOwnProperty("x"),A=d.hasOwnProperty("y"),T=r,C=n,O=window;if(p){var x=K(i),L="clientHeight",S="clientWidth";x===k(i)&&"static"!==z(x=q(i)).position&&"absolute"===u&&(L="scrollHeight",S="scrollWidth"),(l===n||(l===r||l===o)&&c===h)&&(C=s,y-=(g&&x===O&&O.visualViewport?O.visualViewport.height:x[L])-a.height,y*=f?1:-1),l!==r&&(l!==n&&l!==s||c!==h)||(T=o,b-=(g&&x===O&&O.visualViewport?O.visualViewport.width:x[S])-a.width,b*=f?1:-1)}var D,$=Object.assign({position:u},p&&tt),I=!0===m?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:M(i*s)/s||0,y:M(n*s)/s||0}}({x:b,y},k(i)):{x:b,y};return b=I.x,y=I.y,f?Object.assign({},$,((D={})[C]=A?"0":"",D[T]=E?"0":"",D.transform=(O.devicePixelRatio||1)<=1?"translate("+b+"px, "+y+"px)":"translate3d("+b+"px, "+y+"px, 0)",D)):Object.assign({},$,((e={})[C]=A?y+"px":"",e[T]=E?b+"px":"",e.transform="",e))}const it={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:I(e.placement),variation:Z(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,et(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,et(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var nt={passive:!0};const st={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=k(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,nt)})),a&&l.addEventListener("resize",i.update,nt),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,nt)})),a&&l.removeEventListener("resize",i.update,nt)}},data:{}};var ot={left:"right",right:"left",bottom:"top",top:"bottom"};function rt(t){return t.replace(/left|right|bottom|top/g,(function(t){return ot[t]}))}var at={start:"end",end:"start"};function lt(t){return t.replace(/start|end/g,(function(t){return at[t]}))}function ct(t){var e=k(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ht(t){return H(q(t)).left+ct(t).scrollLeft}function dt(t){var e=z(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function ut(t){return["html","body","#document"].indexOf(x(t))>=0?t.ownerDocument.body:S(t)&&dt(t)?t:ut(V(t))}function ft(t,e){var i;void 0===e&&(e=[]);var n=ut(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=k(n),r=s?[o].concat(o.visualViewport||[],dt(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(ft(V(r)))}function pt(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function mt(t,e,i){return e===u?pt(function(t,e){var i=k(t),n=q(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=F();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+ht(t),y:l}}(t,i)):L(e)?function(t,e){var i=H(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):pt(function(t){var e,i=q(t),n=ct(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=N(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=N(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ht(t),l=-n.scrollTop;return"rtl"===z(s||i).direction&&(a+=N(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(q(t)))}function gt(t){var e,i=t.reference,a=t.element,l=t.placement,d=l?I(l):null,u=l?Z(l):null,f=i.x+i.width/2-a.width/2,p=i.y+i.height/2-a.height/2;switch(d){case n:e={x:f,y:i.y-a.height};break;case s:e={x:f,y:i.y+i.height};break;case o:e={x:i.x+i.width,y:p};break;case r:e={x:i.x-a.width,y:p};break;default:e={x:i.x,y:i.y}}var m=d?Q(d):null;if(null!=m){var g="y"===m?"height":"width";switch(u){case c:e[m]=e[m]-(i[g]/2-a[g]/2);break;case h:e[m]=e[m]+(i[g]/2-a[g]/2)}}return e}function _t(t,e){void 0===e&&(e={});var i=e,r=i.placement,a=void 0===r?t.placement:r,c=i.strategy,h=void 0===c?t.strategy:c,m=i.boundary,g=void 0===m?d:m,_=i.rootBoundary,b=void 0===_?u:_,v=i.elementContext,y=void 0===v?f:v,w=i.altBoundary,E=void 0!==w&&w,A=i.padding,T=void 0===A?0:A,C=U("number"!=typeof T?T:G(T,l)),O=y===f?p:f,k=t.rects.popper,D=t.elements[E?O:y],$=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=ft(V(t)),i=["absolute","fixed"].indexOf(z(t).position)>=0&&S(t)?K(t):t;return L(i)?e.filter((function(t){return L(t)&&W(t,i)&&"body"!==x(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=mt(t,i,n);return e.top=N(s.top,e.top),e.right=P(s.right,e.right),e.bottom=P(s.bottom,e.bottom),e.left=N(s.left,e.left),e}),mt(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(L(D)?D:D.contextElement||q(t.elements.popper),g,b,h),I=H(t.elements.reference),M=gt({reference:I,element:k,strategy:"absolute",placement:a}),j=pt(Object.assign({},k,M)),F=y===f?j:I,B={top:$.top-F.top+C.top,bottom:F.bottom-$.bottom+C.bottom,left:$.left-F.left+C.left,right:F.right-$.right+C.right},R=t.modifiersData.offset;if(y===f&&R){var Y=R[a];Object.keys(B).forEach((function(t){var e=[o,s].indexOf(t)>=0?1:-1,i=[n,s].indexOf(t)>=0?"y":"x";B[t]+=Y[i]*e}))}return B}const bt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,h=t.name;if(!e.modifiersData[h]._skip){for(var d=i.mainAxis,u=void 0===d||d,f=i.altAxis,p=void 0===f||f,_=i.fallbackPlacements,b=i.padding,v=i.boundary,y=i.rootBoundary,w=i.altBoundary,E=i.flipVariations,A=void 0===E||E,T=i.allowedAutoPlacements,C=e.options.placement,O=I(C),x=_||(O!==C&&A?function(t){if(I(t)===a)return[];var e=rt(t);return[lt(t),e,lt(e)]}(C):[rt(C)]),k=[C].concat(x).reduce((function(t,i){return t.concat(I(i)===a?function(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,c=i.allowedAutoPlacements,h=void 0===c?g:c,d=Z(n),u=d?a?m:m.filter((function(t){return Z(t)===d})):l,f=u.filter((function(t){return h.indexOf(t)>=0}));0===f.length&&(f=u);var p=f.reduce((function(e,i){return e[i]=_t(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[I(i)],e}),{});return Object.keys(p).sort((function(t,e){return p[t]-p[e]}))}(e,{placement:i,boundary:v,rootBoundary:y,padding:b,flipVariations:A,allowedAutoPlacements:T}):i)}),[]),L=e.rects.reference,S=e.rects.popper,D=new Map,$=!0,N=k[0],P=0;P=0,B=H?"width":"height",W=_t(e,{placement:M,boundary:v,rootBoundary:y,altBoundary:w,padding:b}),z=H?F?o:r:F?s:n;L[B]>S[B]&&(z=rt(z));var R=rt(z),q=[];if(u&&q.push(W[j]<=0),p&&q.push(W[z]<=0,W[R]<=0),q.every((function(t){return t}))){N=M,$=!1;break}D.set(M,q)}if($)for(var V=function(t){var e=k.find((function(e){var i=D.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return N=e,"break"},Y=A?3:1;Y>0&&"break"!==V(Y);Y--);e.placement!==N&&(e.modifiersData[h]._skip=!0,e.placement=N,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function vt(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function yt(t){return[n,o,s,r].some((function(e){return t[e]>=0}))}const wt={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=_t(e,{elementContext:"reference"}),a=_t(e,{altBoundary:!0}),l=vt(r,n),c=vt(a,s,o),h=yt(l),d=yt(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Et={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,s=t.name,a=i.offset,l=void 0===a?[0,0]:a,c=g.reduce((function(t,i){return t[i]=function(t,e,i){var s=I(t),a=[r,n].indexOf(s)>=0?-1:1,l="function"==typeof i?i(Object.assign({},e,{placement:t})):i,c=l[0],h=l[1];return c=c||0,h=(h||0)*a,[r,o].indexOf(s)>=0?{x:h,y:c}:{x:c,y:h}}(i,e.rects,l),t}),{}),h=c[e.placement],d=h.x,u=h.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=d,e.modifiersData.popperOffsets.y+=u),e.modifiersData[s]=c}},At={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=gt({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},Tt={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,a=t.name,l=i.mainAxis,h=void 0===l||l,d=i.altAxis,u=void 0!==d&&d,f=i.boundary,p=i.rootBoundary,m=i.altBoundary,g=i.padding,_=i.tether,b=void 0===_||_,v=i.tetherOffset,y=void 0===v?0:v,w=_t(e,{boundary:f,rootBoundary:p,padding:g,altBoundary:m}),E=I(e.placement),A=Z(e.placement),T=!A,C=Q(E),O="x"===C?"y":"x",x=e.modifiersData.popperOffsets,k=e.rects.reference,L=e.rects.popper,S="function"==typeof y?y(Object.assign({},e.rects,{placement:e.placement})):y,D="number"==typeof S?{mainAxis:S,altAxis:S}:Object.assign({mainAxis:0,altAxis:0},S),$=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,M={x:0,y:0};if(x){if(h){var j,F="y"===C?n:r,H="y"===C?s:o,W="y"===C?"height":"width",z=x[C],R=z+w[F],q=z-w[H],V=b?-L[W]/2:0,Y=A===c?k[W]:L[W],U=A===c?-L[W]:-k[W],G=e.elements.arrow,J=b&&G?B(G):{width:0,height:0},tt=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},et=tt[F],it=tt[H],nt=X(0,k[W],J[W]),st=T?k[W]/2-V-nt-et-D.mainAxis:Y-nt-et-D.mainAxis,ot=T?-k[W]/2+V+nt+it+D.mainAxis:U+nt+it+D.mainAxis,rt=e.elements.arrow&&K(e.elements.arrow),at=rt?"y"===C?rt.clientTop||0:rt.clientLeft||0:0,lt=null!=(j=null==$?void 0:$[C])?j:0,ct=z+ot-lt,ht=X(b?P(R,z+st-lt-at):R,z,b?N(q,ct):q);x[C]=ht,M[C]=ht-z}if(u){var dt,ut="x"===C?n:r,ft="x"===C?s:o,pt=x[O],mt="y"===O?"height":"width",gt=pt+w[ut],bt=pt-w[ft],vt=-1!==[n,r].indexOf(E),yt=null!=(dt=null==$?void 0:$[O])?dt:0,wt=vt?gt:pt-k[mt]-L[mt]-yt+D.altAxis,Et=vt?pt+k[mt]+L[mt]-yt-D.altAxis:bt,At=b&&vt?function(t,e,i){var n=X(t,e,i);return n>i?i:n}(wt,pt,Et):X(b?wt:gt,pt,b?Et:bt);x[O]=At,M[O]=At-pt}e.modifiersData[a]=M}},requiresIfExists:["offset"]};function Ct(t,e,i){void 0===i&&(i=!1);var n,s,o=S(e),r=S(e)&&function(t){var e=t.getBoundingClientRect(),i=M(e.width)/t.offsetWidth||1,n=M(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=q(e),l=H(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==x(e)||dt(a))&&(c=(n=e)!==k(n)&&S(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:ct(n)),S(e)?((h=H(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=ht(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function Ot(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var xt={placement:"bottom",modifiers:[],strategy:"absolute"};function kt(){for(var t=arguments.length,e=new Array(t),i=0;iIt.has(t)&&It.get(t).get(e)||null,remove(t,e){if(!It.has(t))return;const i=It.get(t);i.delete(e),0===i.size&&It.delete(t)}},Pt="transitionend",Mt=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),jt=t=>{t.dispatchEvent(new Event(Pt))},Ft=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),Ht=t=>Ft(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(Mt(t)):null,Bt=t=>{if(!Ft(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},Wt=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),zt=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?zt(t.parentNode):null},Rt=()=>{},qt=t=>{t.offsetHeight},Vt=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,Yt=[],Kt=()=>"rtl"===document.documentElement.dir,Qt=t=>{var e;e=()=>{const e=Vt();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(Yt.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of Yt)t()})),Yt.push(e)):e()},Xt=(t,e=[],i=t)=>"function"==typeof t?t(...e):i,Ut=(t,e,i=!0)=>{if(!i)return void Xt(t);const n=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let s=!1;const o=({target:i})=>{i===e&&(s=!0,e.removeEventListener(Pt,o),Xt(t))};e.addEventListener(Pt,o),setTimeout((()=>{s||jt(e)}),n)},Gt=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},Jt=/[^.]*(?=\..*)\.|.*/,Zt=/\..*/,te=/::\d+$/,ee={};let ie=1;const ne={mouseenter:"mouseover",mouseleave:"mouseout"},se=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function oe(t,e){return e&&`${e}::${ie++}`||t.uidEvent||ie++}function re(t){const e=oe(t);return t.uidEvent=e,ee[e]=ee[e]||{},ee[e]}function ae(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function le(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=ue(t);return se.has(o)||(o=t),[n,s,o]}function ce(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=le(e,i,n);if(e in ne){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=re(t),c=l[a]||(l[a]={}),h=ae(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=oe(r,e.replace(Jt,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return pe(s,{delegateTarget:r}),n.oneOff&&fe.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return pe(n,{delegateTarget:t}),i.oneOff&&fe.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function he(t,e,i,n,s){const o=ae(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function de(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&he(t,e,i,r.callable,r.delegationSelector)}function ue(t){return t=t.replace(Zt,""),ne[t]||t}const fe={on(t,e,i,n){ce(t,e,i,n,!1)},one(t,e,i,n){ce(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=le(e,i,n),a=r!==e,l=re(t),c=l[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))de(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(te,"");a&&!e.includes(s)||he(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;he(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=Vt();let s=null,o=!0,r=!0,a=!1;e!==ue(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=pe(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function pe(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function me(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function ge(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const _e={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${ge(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${ge(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith("bs")&&!t.startsWith("bsConfig")));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=me(t.dataset[n])}return e},getDataAttribute:(t,e)=>me(t.getAttribute(`data-bs-${ge(e)}`))};class be{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=Ft(e)?_e.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...Ft(e)?_e.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],o=Ft(e)?"element":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(o))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${n}" provided type "${o}" but expected type "${s}".`)}var i}}class ve extends be{constructor(t,e){super(),(t=Ht(t))&&(this._element=t,this._config=this._getConfig(e),Nt.set(this._element,this.constructor.DATA_KEY,this))}dispose(){Nt.remove(this._element,this.constructor.DATA_KEY),fe.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){Ut(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return Nt.get(Ht(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.2"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const ye=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?Mt(i.trim()):null}return e},we={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(",");return this.find(e,t).filter((t=>!Wt(t)&&Bt(t)))},getSelectorFromElement(t){const e=ye(t);return e&&we.findOne(e)?e:null},getElementFromSelector(t){const e=ye(t);return e?we.findOne(e):null},getMultipleElementsFromSelector(t){const e=ye(t);return e?we.find(e):[]}},Ee=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;fe.on(document,i,`[data-bs-dismiss="${n}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),Wt(this))return;const s=we.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()}))},Ae=".bs.alert",Te=`close${Ae}`,Ce=`closed${Ae}`;class Oe extends ve{static get NAME(){return"alert"}close(){if(fe.trigger(this._element,Te).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),fe.trigger(this._element,Ce),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Oe.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}Ee(Oe,"close"),Qt(Oe);const xe='[data-bs-toggle="button"]';class ke extends ve{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=ke.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}fe.on(document,"click.bs.button.data-api",xe,(t=>{t.preventDefault();const e=t.target.closest(xe);ke.getOrCreateInstance(e).toggle()})),Qt(ke);const Le=".bs.swipe",Se=`touchstart${Le}`,De=`touchmove${Le}`,$e=`touchend${Le}`,Ie=`pointerdown${Le}`,Ne=`pointerup${Le}`,Pe={endCallback:null,leftCallback:null,rightCallback:null},Me={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class je extends be{constructor(t,e){super(),this._element=t,t&&je.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return Pe}static get DefaultType(){return Me}static get NAME(){return"swipe"}dispose(){fe.off(this._element,Le)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),Xt(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&Xt(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(fe.on(this._element,Ie,(t=>this._start(t))),fe.on(this._element,Ne,(t=>this._end(t))),this._element.classList.add("pointer-event")):(fe.on(this._element,Se,(t=>this._start(t))),fe.on(this._element,De,(t=>this._move(t))),fe.on(this._element,$e,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const Fe=".bs.carousel",He=".data-api",Be="next",We="prev",ze="left",Re="right",qe=`slide${Fe}`,Ve=`slid${Fe}`,Ye=`keydown${Fe}`,Ke=`mouseenter${Fe}`,Qe=`mouseleave${Fe}`,Xe=`dragstart${Fe}`,Ue=`load${Fe}${He}`,Ge=`click${Fe}${He}`,Je="carousel",Ze="active",ti=".active",ei=".carousel-item",ii=ti+ei,ni={ArrowLeft:Re,ArrowRight:ze},si={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},oi={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class ri extends ve{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=we.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===Je&&this.cycle()}static get Default(){return si}static get DefaultType(){return oi}static get NAME(){return"carousel"}next(){this._slide(Be)}nextWhenVisible(){!document.hidden&&Bt(this._element)&&this.next()}prev(){this._slide(We)}pause(){this._isSliding&&jt(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?fe.one(this._element,Ve,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void fe.one(this._element,Ve,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?Be:We;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&fe.on(this._element,Ye,(t=>this._keydown(t))),"hover"===this._config.pause&&(fe.on(this._element,Ke,(()=>this.pause())),fe.on(this._element,Qe,(()=>this._maybeEnableCycle()))),this._config.touch&&je.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of we.find(".carousel-item img",this._element))fe.on(t,Xe,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ze)),rightCallback:()=>this._slide(this._directionToOrder(Re)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new je(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=ni[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=we.findOne(ti,this._indicatorsElement);e.classList.remove(Ze),e.removeAttribute("aria-current");const i=we.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(Ze),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===Be,s=e||Gt(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>fe.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(qe).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),qt(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(Ze),i.classList.remove(Ze,c,l),this._isSliding=!1,r(Ve)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return we.findOne(ii,this._element)}_getItems(){return we.find(ei,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return Kt()?t===ze?We:Be:t===ze?Be:We}_orderToDirection(t){return Kt()?t===We?ze:Re:t===We?Re:ze}static jQueryInterface(t){return this.each((function(){const e=ri.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)}))}}fe.on(document,Ge,"[data-bs-slide], [data-bs-slide-to]",(function(t){const e=we.getElementFromSelector(this);if(!e||!e.classList.contains(Je))return;t.preventDefault();const i=ri.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===_e.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),fe.on(window,Ue,(()=>{const t=we.find('[data-bs-ride="carousel"]');for(const e of t)ri.getOrCreateInstance(e)})),Qt(ri);const ai=".bs.collapse",li=`show${ai}`,ci=`shown${ai}`,hi=`hide${ai}`,di=`hidden${ai}`,ui=`click${ai}.data-api`,fi="show",pi="collapse",mi="collapsing",gi=`:scope .${pi} .${pi}`,_i='[data-bs-toggle="collapse"]',bi={parent:null,toggle:!0},vi={parent:"(null|element)",toggle:"boolean"};class yi extends ve{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=we.find(_i);for(const t of i){const e=we.getSelectorFromElement(t),i=we.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return bi}static get DefaultType(){return vi}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter((t=>t!==this._element)).map((t=>yi.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(fe.trigger(this._element,li).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(pi),this._element.classList.add(mi),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(mi),this._element.classList.add(pi,fi),this._element.style[e]="",fe.trigger(this._element,ci)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(fe.trigger(this._element,hi).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,qt(this._element),this._element.classList.add(mi),this._element.classList.remove(pi,fi);for(const t of this._triggerArray){const e=we.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(mi),this._element.classList.add(pi),fe.trigger(this._element,di)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(fi)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=Ht(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(_i);for(const e of t){const t=we.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=we.find(gi,this._config.parent);return we.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=yi.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}fe.on(document,ui,_i,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of we.getMultipleElementsFromSelector(this))yi.getOrCreateInstance(t,{toggle:!1}).toggle()})),Qt(yi);const wi="dropdown",Ei=".bs.dropdown",Ai=".data-api",Ti="ArrowUp",Ci="ArrowDown",Oi=`hide${Ei}`,xi=`hidden${Ei}`,ki=`show${Ei}`,Li=`shown${Ei}`,Si=`click${Ei}${Ai}`,Di=`keydown${Ei}${Ai}`,$i=`keyup${Ei}${Ai}`,Ii="show",Ni='[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)',Pi=`${Ni}.${Ii}`,Mi=".dropdown-menu",ji=Kt()?"top-end":"top-start",Fi=Kt()?"top-start":"top-end",Hi=Kt()?"bottom-end":"bottom-start",Bi=Kt()?"bottom-start":"bottom-end",Wi=Kt()?"left-start":"right-start",zi=Kt()?"right-start":"left-start",Ri={autoClose:!0,boundary:"clippingParents",display:"dynamic",offset:[0,2],popperConfig:null,reference:"toggle"},qi={autoClose:"(boolean|string)",boundary:"(string|element)",display:"string",offset:"(array|string|function)",popperConfig:"(null|object|function)",reference:"(string|element|object)"};class Vi extends ve{constructor(t,e){super(t,e),this._popper=null,this._parent=this._element.parentNode,this._menu=we.next(this._element,Mi)[0]||we.prev(this._element,Mi)[0]||we.findOne(Mi,this._parent),this._inNavbar=this._detectNavbar()}static get Default(){return Ri}static get DefaultType(){return qi}static get NAME(){return wi}toggle(){return this._isShown()?this.hide():this.show()}show(){if(Wt(this._element)||this._isShown())return;const t={relatedTarget:this._element};if(!fe.trigger(this._element,ki,t).defaultPrevented){if(this._createPopper(),"ontouchstart"in document.documentElement&&!this._parent.closest(".navbar-nav"))for(const t of[].concat(...document.body.children))fe.on(t,"mouseover",Rt);this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Ii),this._element.classList.add(Ii),fe.trigger(this._element,Li,t)}}hide(){if(Wt(this._element)||!this._isShown())return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){if(!fe.trigger(this._element,Oi,t).defaultPrevented){if("ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.off(t,"mouseover",Rt);this._popper&&this._popper.destroy(),this._menu.classList.remove(Ii),this._element.classList.remove(Ii),this._element.setAttribute("aria-expanded","false"),_e.removeDataAttribute(this._menu,"popper"),fe.trigger(this._element,xi,t)}}_getConfig(t){if("object"==typeof(t=super._getConfig(t)).reference&&!Ft(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${wi.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(){if(void 0===e)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let t=this._element;"parent"===this._config.reference?t=this._parent:Ft(this._config.reference)?t=Ht(this._config.reference):"object"==typeof this._config.reference&&(t=this._config.reference);const i=this._getPopperConfig();this._popper=Dt(t,this._menu,i)}_isShown(){return this._menu.classList.contains(Ii)}_getPlacement(){const t=this._parent;if(t.classList.contains("dropend"))return Wi;if(t.classList.contains("dropstart"))return zi;if(t.classList.contains("dropup-center"))return"top";if(t.classList.contains("dropdown-center"))return"bottom";const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?Fi:ji:e?Bi:Hi}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(_e.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,...Xt(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=we.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter((t=>Bt(t)));i.length&&Gt(i,e,t===Ci,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=Vi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=we.find(Pi);for(const i of e){const e=Vi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Ti,Ci].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ni)?this:we.prev(this,Ni)[0]||we.next(this,Ni)[0]||we.findOne(Ni,t.delegateTarget.parentNode),o=Vi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}fe.on(document,Di,Ni,Vi.dataApiKeydownHandler),fe.on(document,Di,Mi,Vi.dataApiKeydownHandler),fe.on(document,Si,Vi.clearMenus),fe.on(document,$i,Vi.clearMenus),fe.on(document,Si,Ni,(function(t){t.preventDefault(),Vi.getOrCreateInstance(this).toggle()})),Qt(Vi);const Yi="backdrop",Ki="show",Qi=`mousedown.bs.${Yi}`,Xi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Ui={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Gi extends be{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Xi}static get DefaultType(){return Ui}static get NAME(){return Yi}show(t){if(!this._config.isVisible)return void Xt(t);this._append();const e=this._getElement();this._config.isAnimated&&qt(e),e.classList.add(Ki),this._emulateAnimation((()=>{Xt(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ki),this._emulateAnimation((()=>{this.dispose(),Xt(t)}))):Xt(t)}dispose(){this._isAppended&&(fe.off(this._element,Qi),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=Ht(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),fe.on(t,Qi,(()=>{Xt(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){Ut(t,this._getElement(),this._config.isAnimated)}}const Ji=".bs.focustrap",Zi=`focusin${Ji}`,tn=`keydown.tab${Ji}`,en="backward",nn={autofocus:!0,trapElement:null},sn={autofocus:"boolean",trapElement:"element"};class on extends be{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return nn}static get DefaultType(){return sn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),fe.off(document,Ji),fe.on(document,Zi,(t=>this._handleFocusin(t))),fe.on(document,tn,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,fe.off(document,Ji))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=we.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===en?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?en:"forward")}}const rn=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",an=".sticky-top",ln="padding-right",cn="margin-right";class hn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,ln,(e=>e+t)),this._setElementAttributes(rn,ln,(e=>e+t)),this._setElementAttributes(an,cn,(e=>e-t))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,ln),this._resetElementAttributes(rn,ln),this._resetElementAttributes(an,cn)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&_e.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=_e.getDataAttribute(t,e);null!==i?(_e.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(Ft(t))e(t);else for(const i of we.find(t,this._element))e(i)}}const dn=".bs.modal",un=`hide${dn}`,fn=`hidePrevented${dn}`,pn=`hidden${dn}`,mn=`show${dn}`,gn=`shown${dn}`,_n=`resize${dn}`,bn=`click.dismiss${dn}`,vn=`mousedown.dismiss${dn}`,yn=`keydown.dismiss${dn}`,wn=`click${dn}.data-api`,En="modal-open",An="show",Tn="modal-static",Cn={backdrop:!0,focus:!0,keyboard:!0},On={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class xn extends ve{constructor(t,e){super(t,e),this._dialog=we.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new hn,this._addEventListeners()}static get Default(){return Cn}static get DefaultType(){return On}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||fe.trigger(this._element,mn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(En),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(fe.trigger(this._element,un).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(An),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){fe.off(window,dn),fe.off(this._dialog,dn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Gi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new on({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=we.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),qt(this._element),this._element.classList.add(An),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,fe.trigger(this._element,gn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){fe.on(this._element,yn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),fe.on(window,_n,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),fe.on(this._element,vn,(t=>{fe.one(this._element,bn,(e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(En),this._resetAdjustments(),this._scrollBar.reset(),fe.trigger(this._element,pn)}))}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(fe.trigger(this._element,fn).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(Tn)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(Tn),this._queueCallback((()=>{this._element.classList.remove(Tn),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=Kt()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=Kt()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=xn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}fe.on(document,wn,'[data-bs-toggle="modal"]',(function(t){const e=we.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),fe.one(e,mn,(t=>{t.defaultPrevented||fe.one(e,pn,(()=>{Bt(this)&&this.focus()}))}));const i=we.findOne(".modal.show");i&&xn.getInstance(i).hide(),xn.getOrCreateInstance(e).toggle(this)})),Ee(xn),Qt(xn);const kn=".bs.offcanvas",Ln=".data-api",Sn=`load${kn}${Ln}`,Dn="show",$n="showing",In="hiding",Nn=".offcanvas.show",Pn=`show${kn}`,Mn=`shown${kn}`,jn=`hide${kn}`,Fn=`hidePrevented${kn}`,Hn=`hidden${kn}`,Bn=`resize${kn}`,Wn=`click${kn}${Ln}`,zn=`keydown.dismiss${kn}`,Rn={backdrop:!0,keyboard:!0,scroll:!1},qn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Vn extends ve{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Rn}static get DefaultType(){return qn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||fe.trigger(this._element,Pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new hn).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add($n),this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Dn),this._element.classList.remove($n),fe.trigger(this._element,Mn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(fe.trigger(this._element,jn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(In),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Dn,In),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new hn).reset(),fe.trigger(this._element,Hn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Gi({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():fe.trigger(this._element,Fn)}:null})}_initializeFocusTrap(){return new on({trapElement:this._element})}_addEventListeners(){fe.on(this._element,zn,(t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():fe.trigger(this._element,Fn))}))}static jQueryInterface(t){return this.each((function(){const e=Vn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}fe.on(document,Wn,'[data-bs-toggle="offcanvas"]',(function(t){const e=we.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),Wt(this))return;fe.one(e,Hn,(()=>{Bt(this)&&this.focus()}));const i=we.findOne(Nn);i&&i!==e&&Vn.getInstance(i).hide(),Vn.getOrCreateInstance(e).toggle(this)})),fe.on(window,Sn,(()=>{for(const t of we.find(Nn))Vn.getOrCreateInstance(t).show()})),fe.on(window,Bn,(()=>{for(const t of we.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&Vn.getOrCreateInstance(t).hide()})),Ee(Vn),Qt(Vn);const Yn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Kn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Qn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Kn.has(i)||Boolean(Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Un={allowList:Yn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
"},Gn={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},Jn={entry:"(string|element|function|null)",selector:"(string|element)"};class Zn extends be{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Un}static get DefaultType(){return Gn}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Jn)}_setContent(t,e,i){const n=we.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?Ft(e)?this._putElementInTemplate(Ht(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return Xt(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const ts=new Set(["sanitize","allowList","sanitizeFn"]),es="fade",is="show",ns=".modal",ss="hide.bs.modal",os="hover",rs="focus",as={AUTO:"auto",TOP:"top",RIGHT:Kt()?"left":"right",BOTTOM:"bottom",LEFT:Kt()?"right":"left"},ls={allowList:Yn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},cs={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class hs extends ve{constructor(t,i){if(void 0===e)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t,i),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return ls}static get DefaultType(){return cs}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),fe.off(this._element.closest(ns),ss,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=fe.trigger(this._element,this.constructor.eventName("show")),e=(zt(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),fe.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(is),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.on(t,"mouseover",Rt);this._queueCallback((()=>{fe.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1}),this.tip,this._isAnimated())}hide(){if(this._isShown()&&!fe.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(is),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))fe.off(t,"mouseover",Rt);this._activeTrigger.click=!1,this._activeTrigger[rs]=!1,this._activeTrigger[os]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),fe.trigger(this._element,this.constructor.eventName("hidden")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(es,is),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(es),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Zn({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{".tooltip-inner":this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(es)}_isShown(){return this.tip&&this.tip.classList.contains(is)}_createPopper(t){const e=Xt(this._config.placement,[this,t,this._element]),i=as[e.toUpperCase()];return Dt(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return Xt(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,...Xt(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)fe.on(this._element,this.constructor.eventName("click"),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if("manual"!==e){const t=e===os?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===os?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");fe.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?rs:os]=!0,e._enter()})),fe.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?rs:os]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},fe.on(this._element.closest(ns),ss,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=_e.getDataAttributes(this._element);for(const t of Object.keys(e))ts.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:Ht(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const e=hs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}Qt(hs);const ds={...hs.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},us={...hs.DefaultType,content:"(null|string|element|function)"};class fs extends hs{static get Default(){return ds}static get DefaultType(){return us}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{".popover-header":this._getTitle(),".popover-body":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=fs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}Qt(fs);const ps=".bs.scrollspy",ms=`activate${ps}`,gs=`click${ps}`,_s=`load${ps}.data-api`,bs="active",vs="[href]",ys=".nav-link",ws=`${ys}, .nav-item > ${ys}, .list-group-item`,Es={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},As={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Ts extends ve{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return Es}static get DefaultType(){return As}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=Ht(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(fe.off(this._config.target,gs),fe.on(this._config.target,gs,vs,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=we.find(vs,this._config.target);for(const e of t){if(!e.hash||Wt(e))continue;const t=we.findOne(decodeURI(e.hash),this._element);Bt(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(bs),this._activateParents(t),fe.trigger(this._element,ms,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))we.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(bs);else for(const e of we.parents(t,".nav, .list-group"))for(const t of we.prev(e,ws))t.classList.add(bs)}_clearActiveClass(t){t.classList.remove(bs);const e=we.find(`${vs}.${bs}`,t);for(const t of e)t.classList.remove(bs)}static jQueryInterface(t){return this.each((function(){const e=Ts.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}fe.on(window,_s,(()=>{for(const t of we.find('[data-bs-spy="scroll"]'))Ts.getOrCreateInstance(t)})),Qt(Ts);const Cs=".bs.tab",Os=`hide${Cs}`,xs=`hidden${Cs}`,ks=`show${Cs}`,Ls=`shown${Cs}`,Ss=`click${Cs}`,Ds=`keydown${Cs}`,$s=`load${Cs}`,Is="ArrowLeft",Ns="ArrowRight",Ps="ArrowUp",Ms="ArrowDown",js="Home",Fs="End",Hs="active",Bs="fade",Ws="show",zs=".dropdown-toggle",Rs=`:not(${zs})`,qs='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Vs=`.nav-link${Rs}, .list-group-item${Rs}, [role="tab"]${Rs}, ${qs}`,Ys=`.${Hs}[data-bs-toggle="tab"], .${Hs}[data-bs-toggle="pill"], .${Hs}[data-bs-toggle="list"]`;class Ks extends ve{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),fe.on(this._element,Ds,(t=>this._keydown(t))))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?fe.trigger(e,Os,{relatedTarget:t}):null;fe.trigger(t,ks,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Hs),this._activate(we.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),fe.trigger(t,Ls,{relatedTarget:e})):t.classList.add(Ws)}),t,t.classList.contains(Bs)))}_deactivate(t,e){t&&(t.classList.remove(Hs),t.blur(),this._deactivate(we.getElementFromSelector(t)),this._queueCallback((()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),fe.trigger(t,xs,{relatedTarget:e})):t.classList.remove(Ws)}),t,t.classList.contains(Bs)))}_keydown(t){if(![Is,Ns,Ps,Ms,js,Fs].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!Wt(t)));let i;if([js,Fs].includes(t.key))i=e[t.key===js?0:e.length-1];else{const n=[Ns,Ms].includes(t.key);i=Gt(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Ks.getOrCreateInstance(i).show())}_getChildren(){return we.find(Vs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=we.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=we.findOne(t,i);s&&s.classList.toggle(n,e)};n(zs,Hs),n(".dropdown-menu",Ws),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Hs)}_getInnerElement(t){return t.matches(Vs)?t:we.findOne(Vs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each((function(){const e=Ks.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}))}}fe.on(document,Ss,qs,(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),Wt(this)||Ks.getOrCreateInstance(this).show()})),fe.on(window,$s,(()=>{for(const t of we.find(Ys))Ks.getOrCreateInstance(t)})),Qt(Ks);const Qs=".bs.toast",Xs=`mouseover${Qs}`,Us=`mouseout${Qs}`,Gs=`focusin${Qs}`,Js=`focusout${Qs}`,Zs=`hide${Qs}`,to=`hidden${Qs}`,eo=`show${Qs}`,io=`shown${Qs}`,no="hide",so="show",oo="showing",ro={animation:"boolean",autohide:"boolean",delay:"number"},ao={animation:!0,autohide:!0,delay:5e3};class lo extends ve{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return ao}static get DefaultType(){return ro}static get NAME(){return"toast"}show(){fe.trigger(this._element,eo).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(no),qt(this._element),this._element.classList.add(so,oo),this._queueCallback((()=>{this._element.classList.remove(oo),fe.trigger(this._element,io),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(fe.trigger(this._element,Zs).defaultPrevented||(this._element.classList.add(oo),this._queueCallback((()=>{this._element.classList.add(no),this._element.classList.remove(oo,so),fe.trigger(this._element,to)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(so),super.dispose()}isShown(){return this._element.classList.contains(so)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){fe.on(this._element,Xs,(t=>this._onInteraction(t,!0))),fe.on(this._element,Us,(t=>this._onInteraction(t,!1))),fe.on(this._element,Gs,(t=>this._onInteraction(t,!0))),fe.on(this._element,Js,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=lo.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}function co(t){"loading"!=document.readyState?t():document.addEventListener("DOMContentLoaded",t)}Ee(lo),Qt(lo),co((function(){[].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')).map((function(t){return new hs(t,{delay:{show:500,hide:100}})}))})),co((function(){document.getElementById("pst-back-to-top").addEventListener("click",(function(){document.body.scrollTop=0,document.documentElement.scrollTop=0}))})),co((function(){var t=document.getElementById("pst-back-to-top"),e=document.getElementsByClassName("bd-header")[0].getBoundingClientRect();window.addEventListener("scroll",(function(){this.oldScroll>this.scrollY&&this.scrollY>e.bottom?t.style.display="block":t.style.display="none",this.oldScroll=this.scrollY}))})),window.bootstrap=i})(); +//# sourceMappingURL=bootstrap.js.map \ No newline at end of file diff --git a/_static/scripts/bootstrap.js.LICENSE.txt b/_static/scripts/bootstrap.js.LICENSE.txt new file mode 100644 index 00000000..10f979d0 --- /dev/null +++ b/_static/scripts/bootstrap.js.LICENSE.txt @@ -0,0 +1,5 @@ +/*! + * Bootstrap v5.3.2 (https://getbootstrap.com/) + * Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */ diff --git a/_static/scripts/bootstrap.js.map b/_static/scripts/bootstrap.js.map new file mode 100644 index 00000000..64e212b1 --- /dev/null +++ b/_static/scripts/bootstrap.js.map @@ -0,0 +1 @@ +{"version":3,"file":"scripts/bootstrap.js","mappings":";mBACA,IAAIA,EAAsB,CCA1BA,EAAwB,CAACC,EAASC,KACjC,IAAI,IAAIC,KAAOD,EACXF,EAAoBI,EAAEF,EAAYC,KAASH,EAAoBI,EAAEH,EAASE,IAC5EE,OAAOC,eAAeL,EAASE,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECNDH,EAAwB,CAACS,EAAKC,IAAUL,OAAOM,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFV,EAAyBC,IACH,oBAAXa,QAA0BA,OAAOC,aAC1CV,OAAOC,eAAeL,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DX,OAAOC,eAAeL,EAAS,aAAc,CAAEe,OAAO,GAAO,01BCLvD,IAAI,EAAM,MACNC,EAAS,SACTC,EAAQ,QACRC,EAAO,OACPC,EAAO,OACPC,EAAiB,CAAC,EAAKJ,EAAQC,EAAOC,GACtCG,EAAQ,QACRC,EAAM,MACNC,EAAkB,kBAClBC,EAAW,WACXC,EAAS,SACTC,EAAY,YACZC,EAAmCP,EAAeQ,QAAO,SAAUC,EAAKC,GACjF,OAAOD,EAAIE,OAAO,CAACD,EAAY,IAAMT,EAAOS,EAAY,IAAMR,GAChE,GAAG,IACQ,EAA0B,GAAGS,OAAOX,EAAgB,CAACD,IAAOS,QAAO,SAAUC,EAAKC,GAC3F,OAAOD,EAAIE,OAAO,CAACD,EAAWA,EAAY,IAAMT,EAAOS,EAAY,IAAMR,GAC3E,GAAG,IAEQU,EAAa,aACbC,EAAO,OACPC,EAAY,YAEZC,EAAa,aACbC,EAAO,OACPC,EAAY,YAEZC,EAAc,cACdC,EAAQ,QACRC,EAAa,aACbC,EAAiB,CAACT,EAAYC,EAAMC,EAAWC,EAAYC,EAAMC,EAAWC,EAAaC,EAAOC,GC9B5F,SAASE,EAAYC,GAClC,OAAOA,GAAWA,EAAQC,UAAY,IAAIC,cAAgB,IAC5D,CCFe,SAASC,EAAUC,GAChC,GAAY,MAARA,EACF,OAAOC,OAGT,GAAwB,oBAApBD,EAAKE,WAAkC,CACzC,IAAIC,EAAgBH,EAAKG,cACzB,OAAOA,GAAgBA,EAAcC,aAAwBH,MAC/D,CAEA,OAAOD,CACT,CCTA,SAASK,EAAUL,GAEjB,OAAOA,aADUD,EAAUC,GAAMM,SACIN,aAAgBM,OACvD,CAEA,SAASC,EAAcP,GAErB,OAAOA,aADUD,EAAUC,GAAMQ,aACIR,aAAgBQ,WACvD,CAEA,SAASC,EAAaT,GAEpB,MAA0B,oBAAfU,aAKJV,aADUD,EAAUC,GAAMU,YACIV,aAAgBU,WACvD,CCwDA,SACEC,KAAM,cACNC,SAAS,EACTC,MAAO,QACPC,GA5EF,SAAqBC,GACnB,IAAIC,EAAQD,EAAKC,MACjB3D,OAAO4D,KAAKD,EAAME,UAAUC,SAAQ,SAAUR,GAC5C,IAAIS,EAAQJ,EAAMK,OAAOV,IAAS,CAAC,EAC/BW,EAAaN,EAAMM,WAAWX,IAAS,CAAC,EACxCf,EAAUoB,EAAME,SAASP,GAExBJ,EAAcX,IAAaD,EAAYC,KAO5CvC,OAAOkE,OAAO3B,EAAQwB,MAAOA,GAC7B/D,OAAO4D,KAAKK,GAAYH,SAAQ,SAAUR,GACxC,IAAI3C,EAAQsD,EAAWX,IAET,IAAV3C,EACF4B,EAAQ4B,gBAAgBb,GAExBf,EAAQ6B,aAAad,GAAgB,IAAV3C,EAAiB,GAAKA,EAErD,IACF,GACF,EAoDE0D,OAlDF,SAAgBC,GACd,IAAIX,EAAQW,EAAMX,MACdY,EAAgB,CAClBlD,OAAQ,CACNmD,SAAUb,EAAMc,QAAQC,SACxB5D,KAAM,IACN6D,IAAK,IACLC,OAAQ,KAEVC,MAAO,CACLL,SAAU,YAEZlD,UAAW,CAAC,GASd,OAPAtB,OAAOkE,OAAOP,EAAME,SAASxC,OAAO0C,MAAOQ,EAAclD,QACzDsC,EAAMK,OAASO,EAEXZ,EAAME,SAASgB,OACjB7E,OAAOkE,OAAOP,EAAME,SAASgB,MAAMd,MAAOQ,EAAcM,OAGnD,WACL7E,OAAO4D,KAAKD,EAAME,UAAUC,SAAQ,SAAUR,GAC5C,IAAIf,EAAUoB,EAAME,SAASP,GACzBW,EAAaN,EAAMM,WAAWX,IAAS,CAAC,EAGxCS,EAFkB/D,OAAO4D,KAAKD,EAAMK,OAAOzD,eAAe+C,GAAQK,EAAMK,OAAOV,GAAQiB,EAAcjB,IAE7E9B,QAAO,SAAUuC,EAAOe,GAElD,OADAf,EAAMe,GAAY,GACXf,CACT,GAAG,CAAC,GAECb,EAAcX,IAAaD,EAAYC,KAI5CvC,OAAOkE,OAAO3B,EAAQwB,MAAOA,GAC7B/D,OAAO4D,KAAKK,GAAYH,SAAQ,SAAUiB,GACxCxC,EAAQ4B,gBAAgBY,EAC1B,IACF,GACF,CACF,EASEC,SAAU,CAAC,kBCjFE,SAASC,EAAiBvD,GACvC,OAAOA,EAAUwD,MAAM,KAAK,EAC9B,CCHO,IAAI,EAAMC,KAAKC,IACX,EAAMD,KAAKE,IACXC,EAAQH,KAAKG,MCFT,SAASC,IACtB,IAAIC,EAASC,UAAUC,cAEvB,OAAc,MAAVF,GAAkBA,EAAOG,QAAUC,MAAMC,QAAQL,EAAOG,QACnDH,EAAOG,OAAOG,KAAI,SAAUC,GACjC,OAAOA,EAAKC,MAAQ,IAAMD,EAAKE,OACjC,IAAGC,KAAK,KAGHT,UAAUU,SACnB,CCTe,SAASC,IACtB,OAAQ,iCAAiCC,KAAKd,IAChD,CCCe,SAASe,EAAsB/D,EAASgE,EAAcC,QAC9C,IAAjBD,IACFA,GAAe,QAGO,IAApBC,IACFA,GAAkB,GAGpB,IAAIC,EAAalE,EAAQ+D,wBACrBI,EAAS,EACTC,EAAS,EAETJ,GAAgBrD,EAAcX,KAChCmE,EAASnE,EAAQqE,YAAc,GAAItB,EAAMmB,EAAWI,OAAStE,EAAQqE,aAAmB,EACxFD,EAASpE,EAAQuE,aAAe,GAAIxB,EAAMmB,EAAWM,QAAUxE,EAAQuE,cAAoB,GAG7F,IACIE,GADOhE,EAAUT,GAAWG,EAAUH,GAAWK,QAC3BoE,eAEtBC,GAAoBb,KAAsBI,EAC1CU,GAAKT,EAAW3F,MAAQmG,GAAoBD,EAAiBA,EAAeG,WAAa,IAAMT,EAC/FU,GAAKX,EAAW9B,KAAOsC,GAAoBD,EAAiBA,EAAeK,UAAY,IAAMV,EAC7FE,EAAQJ,EAAWI,MAAQH,EAC3BK,EAASN,EAAWM,OAASJ,EACjC,MAAO,CACLE,MAAOA,EACPE,OAAQA,EACRpC,IAAKyC,EACLvG,MAAOqG,EAAIL,EACXjG,OAAQwG,EAAIL,EACZjG,KAAMoG,EACNA,EAAGA,EACHE,EAAGA,EAEP,CCrCe,SAASE,EAAc/E,GACpC,IAAIkE,EAAaH,EAAsB/D,GAGnCsE,EAAQtE,EAAQqE,YAChBG,EAASxE,EAAQuE,aAUrB,OARI3B,KAAKoC,IAAId,EAAWI,MAAQA,IAAU,IACxCA,EAAQJ,EAAWI,OAGjB1B,KAAKoC,IAAId,EAAWM,OAASA,IAAW,IAC1CA,EAASN,EAAWM,QAGf,CACLG,EAAG3E,EAAQ4E,WACXC,EAAG7E,EAAQ8E,UACXR,MAAOA,EACPE,OAAQA,EAEZ,CCvBe,SAASS,EAASC,EAAQC,GACvC,IAAIC,EAAWD,EAAME,aAAeF,EAAME,cAE1C,GAAIH,EAAOD,SAASE,GAClB,OAAO,EAEJ,GAAIC,GAAYvE,EAAauE,GAAW,CACzC,IAAIE,EAAOH,EAEX,EAAG,CACD,GAAIG,GAAQJ,EAAOK,WAAWD,GAC5B,OAAO,EAITA,EAAOA,EAAKE,YAAcF,EAAKG,IACjC,OAASH,EACX,CAGF,OAAO,CACT,CCrBe,SAAS,EAAiBtF,GACvC,OAAOG,EAAUH,GAAS0F,iBAAiB1F,EAC7C,CCFe,SAAS2F,EAAe3F,GACrC,MAAO,CAAC,QAAS,KAAM,MAAM4F,QAAQ7F,EAAYC,KAAa,CAChE,CCFe,SAAS6F,EAAmB7F,GAEzC,QAASS,EAAUT,GAAWA,EAAQO,cACtCP,EAAQ8F,WAAazF,OAAOyF,UAAUC,eACxC,CCFe,SAASC,EAAchG,GACpC,MAA6B,SAAzBD,EAAYC,GACPA,EAMPA,EAAQiG,cACRjG,EAAQwF,aACR3E,EAAab,GAAWA,EAAQyF,KAAO,OAEvCI,EAAmB7F,EAGvB,CCVA,SAASkG,EAAoBlG,GAC3B,OAAKW,EAAcX,IACoB,UAAvC,EAAiBA,GAASiC,SAInBjC,EAAQmG,aAHN,IAIX,CAwCe,SAASC,EAAgBpG,GAItC,IAHA,IAAIK,EAASF,EAAUH,GACnBmG,EAAeD,EAAoBlG,GAEhCmG,GAAgBR,EAAeQ,IAA6D,WAA5C,EAAiBA,GAAclE,UACpFkE,EAAeD,EAAoBC,GAGrC,OAAIA,IAA+C,SAA9BpG,EAAYoG,IAA0D,SAA9BpG,EAAYoG,IAAwE,WAA5C,EAAiBA,GAAclE,UAC3H5B,EAGF8F,GAhDT,SAA4BnG,GAC1B,IAAIqG,EAAY,WAAWvC,KAAKd,KAGhC,GAFW,WAAWc,KAAKd,MAEfrC,EAAcX,IAII,UAFX,EAAiBA,GAEnBiC,SACb,OAAO,KAIX,IAAIqE,EAAcN,EAAchG,GAMhC,IAJIa,EAAayF,KACfA,EAAcA,EAAYb,MAGrB9E,EAAc2F,IAAgB,CAAC,OAAQ,QAAQV,QAAQ7F,EAAYuG,IAAgB,GAAG,CAC3F,IAAIC,EAAM,EAAiBD,GAI3B,GAAsB,SAAlBC,EAAIC,WAA4C,SAApBD,EAAIE,aAA0C,UAAhBF,EAAIG,UAAiF,IAA1D,CAAC,YAAa,eAAed,QAAQW,EAAII,aAAsBN,GAAgC,WAAnBE,EAAII,YAA2BN,GAAaE,EAAIK,QAAyB,SAAfL,EAAIK,OACjO,OAAON,EAEPA,EAAcA,EAAYd,UAE9B,CAEA,OAAO,IACT,CAgByBqB,CAAmB7G,IAAYK,CACxD,CCpEe,SAASyG,EAAyB3H,GAC/C,MAAO,CAAC,MAAO,UAAUyG,QAAQzG,IAAc,EAAI,IAAM,GAC3D,CCDO,SAAS4H,EAAOjE,EAAK1E,EAAOyE,GACjC,OAAO,EAAQC,EAAK,EAAQ1E,EAAOyE,GACrC,CCFe,SAASmE,EAAmBC,GACzC,OAAOxJ,OAAOkE,OAAO,CAAC,ECDf,CACLS,IAAK,EACL9D,MAAO,EACPD,OAAQ,EACRE,KAAM,GDHuC0I,EACjD,CEHe,SAASC,EAAgB9I,EAAOiD,GAC7C,OAAOA,EAAKpC,QAAO,SAAUkI,EAAS5J,GAEpC,OADA4J,EAAQ5J,GAAOa,EACR+I,CACT,GAAG,CAAC,EACN,CC4EA,SACEpG,KAAM,QACNC,SAAS,EACTC,MAAO,OACPC,GApEF,SAAeC,GACb,IAAIiG,EAEAhG,EAAQD,EAAKC,MACbL,EAAOI,EAAKJ,KACZmB,EAAUf,EAAKe,QACfmF,EAAejG,EAAME,SAASgB,MAC9BgF,EAAgBlG,EAAMmG,cAAcD,cACpCE,EAAgB9E,EAAiBtB,EAAMjC,WACvCsI,EAAOX,EAAyBU,GAEhCE,EADa,CAACnJ,EAAMD,GAAOsH,QAAQ4B,IAAkB,EAClC,SAAW,QAElC,GAAKH,GAAiBC,EAAtB,CAIA,IAAIL,EAxBgB,SAAyBU,EAASvG,GAItD,OAAO4F,EAAsC,iBAH7CW,EAA6B,mBAAZA,EAAyBA,EAAQlK,OAAOkE,OAAO,CAAC,EAAGP,EAAMwG,MAAO,CAC/EzI,UAAWiC,EAAMjC,aACbwI,GACkDA,EAAUT,EAAgBS,EAASlJ,GAC7F,CAmBsBoJ,CAAgB3F,EAAQyF,QAASvG,GACjD0G,EAAY/C,EAAcsC,GAC1BU,EAAmB,MAATN,EAAe,EAAMlJ,EAC/ByJ,EAAmB,MAATP,EAAepJ,EAASC,EAClC2J,EAAU7G,EAAMwG,MAAM7I,UAAU2I,GAAOtG,EAAMwG,MAAM7I,UAAU0I,GAAQH,EAAcG,GAAQrG,EAAMwG,MAAM9I,OAAO4I,GAC9GQ,EAAYZ,EAAcG,GAAQrG,EAAMwG,MAAM7I,UAAU0I,GACxDU,EAAoB/B,EAAgBiB,GACpCe,EAAaD,EAA6B,MAATV,EAAeU,EAAkBE,cAAgB,EAAIF,EAAkBG,aAAe,EAAI,EAC3HC,EAAoBN,EAAU,EAAIC,EAAY,EAG9CpF,EAAMmE,EAAcc,GACpBlF,EAAMuF,EAAaN,EAAUJ,GAAOT,EAAce,GAClDQ,EAASJ,EAAa,EAAIN,EAAUJ,GAAO,EAAIa,EAC/CE,EAAS1B,EAAOjE,EAAK0F,EAAQ3F,GAE7B6F,EAAWjB,EACfrG,EAAMmG,cAAcxG,KAASqG,EAAwB,CAAC,GAAyBsB,GAAYD,EAAQrB,EAAsBuB,aAAeF,EAASD,EAAQpB,EAnBzJ,CAoBF,EAkCEtF,OAhCF,SAAgBC,GACd,IAAIX,EAAQW,EAAMX,MAEdwH,EADU7G,EAAMG,QACWlC,QAC3BqH,OAAoC,IAArBuB,EAA8B,sBAAwBA,EAErD,MAAhBvB,IAKwB,iBAAjBA,IACTA,EAAejG,EAAME,SAASxC,OAAO+J,cAAcxB,MAOhDpC,EAAS7D,EAAME,SAASxC,OAAQuI,KAIrCjG,EAAME,SAASgB,MAAQ+E,EACzB,EASE5E,SAAU,CAAC,iBACXqG,iBAAkB,CAAC,oBCxFN,SAASC,EAAa5J,GACnC,OAAOA,EAAUwD,MAAM,KAAK,EAC9B,CCOA,IAAIqG,GAAa,CACf5G,IAAK,OACL9D,MAAO,OACPD,OAAQ,OACRE,KAAM,QAeD,SAAS0K,GAAYlH,GAC1B,IAAImH,EAEApK,EAASiD,EAAMjD,OACfqK,EAAapH,EAAMoH,WACnBhK,EAAY4C,EAAM5C,UAClBiK,EAAYrH,EAAMqH,UAClBC,EAAUtH,EAAMsH,QAChBpH,EAAWF,EAAME,SACjBqH,EAAkBvH,EAAMuH,gBACxBC,EAAWxH,EAAMwH,SACjBC,EAAezH,EAAMyH,aACrBC,EAAU1H,EAAM0H,QAChBC,EAAaL,EAAQ1E,EACrBA,OAAmB,IAAf+E,EAAwB,EAAIA,EAChCC,EAAaN,EAAQxE,EACrBA,OAAmB,IAAf8E,EAAwB,EAAIA,EAEhCC,EAAgC,mBAAjBJ,EAA8BA,EAAa,CAC5D7E,EAAGA,EACHE,IACG,CACHF,EAAGA,EACHE,GAGFF,EAAIiF,EAAMjF,EACVE,EAAI+E,EAAM/E,EACV,IAAIgF,EAAOR,EAAQrL,eAAe,KAC9B8L,EAAOT,EAAQrL,eAAe,KAC9B+L,EAAQxL,EACRyL,EAAQ,EACRC,EAAM5J,OAEV,GAAIkJ,EAAU,CACZ,IAAIpD,EAAeC,EAAgBtH,GAC/BoL,EAAa,eACbC,EAAY,cAEZhE,IAAiBhG,EAAUrB,IAGmB,WAA5C,EAFJqH,EAAeN,EAAmB/G,IAECmD,UAAsC,aAAbA,IAC1DiI,EAAa,eACbC,EAAY,gBAOZhL,IAAc,IAAQA,IAAcZ,GAAQY,IAAcb,IAAU8K,IAAczK,KACpFqL,EAAQ3L,EAGRwG,IAFc4E,GAAWtD,IAAiB8D,GAAOA,EAAIxF,eAAiBwF,EAAIxF,eAAeD,OACzF2B,EAAa+D,IACEf,EAAW3E,OAC1BK,GAAKyE,EAAkB,GAAK,GAG1BnK,IAAcZ,IAASY,IAAc,GAAOA,IAAcd,GAAW+K,IAAczK,KACrFoL,EAAQzL,EAGRqG,IAFc8E,GAAWtD,IAAiB8D,GAAOA,EAAIxF,eAAiBwF,EAAIxF,eAAeH,MACzF6B,EAAagE,IACEhB,EAAW7E,MAC1BK,GAAK2E,EAAkB,GAAK,EAEhC,CAEA,IAgBMc,EAhBFC,EAAe5M,OAAOkE,OAAO,CAC/BM,SAAUA,GACTsH,GAAYP,IAEXsB,GAAyB,IAAjBd,EAlFd,SAA2BrI,EAAM8I,GAC/B,IAAItF,EAAIxD,EAAKwD,EACTE,EAAI1D,EAAK0D,EACT0F,EAAMN,EAAIO,kBAAoB,EAClC,MAAO,CACL7F,EAAG5B,EAAM4B,EAAI4F,GAAOA,GAAO,EAC3B1F,EAAG9B,EAAM8B,EAAI0F,GAAOA,GAAO,EAE/B,CA0EsCE,CAAkB,CACpD9F,EAAGA,EACHE,GACC1E,EAAUrB,IAAW,CACtB6F,EAAGA,EACHE,GAMF,OAHAF,EAAI2F,EAAM3F,EACVE,EAAIyF,EAAMzF,EAENyE,EAGK7L,OAAOkE,OAAO,CAAC,EAAG0I,IAAeD,EAAiB,CAAC,GAAkBJ,GAASF,EAAO,IAAM,GAAIM,EAAeL,GAASF,EAAO,IAAM,GAAIO,EAAe5D,WAAayD,EAAIO,kBAAoB,IAAM,EAAI,aAAe7F,EAAI,OAASE,EAAI,MAAQ,eAAiBF,EAAI,OAASE,EAAI,SAAUuF,IAG5R3M,OAAOkE,OAAO,CAAC,EAAG0I,IAAenB,EAAkB,CAAC,GAAmBc,GAASF,EAAOjF,EAAI,KAAO,GAAIqE,EAAgBa,GAASF,EAAOlF,EAAI,KAAO,GAAIuE,EAAgB1C,UAAY,GAAI0C,GAC9L,CA4CA,UACEnI,KAAM,gBACNC,SAAS,EACTC,MAAO,cACPC,GA9CF,SAAuBwJ,GACrB,IAAItJ,EAAQsJ,EAAMtJ,MACdc,EAAUwI,EAAMxI,QAChByI,EAAwBzI,EAAQoH,gBAChCA,OAA4C,IAA1BqB,GAA0CA,EAC5DC,EAAoB1I,EAAQqH,SAC5BA,OAAiC,IAAtBqB,GAAsCA,EACjDC,EAAwB3I,EAAQsH,aAChCA,OAAyC,IAA1BqB,GAA0CA,EACzDR,EAAe,CACjBlL,UAAWuD,EAAiBtB,EAAMjC,WAClCiK,UAAWL,EAAa3H,EAAMjC,WAC9BL,OAAQsC,EAAME,SAASxC,OACvBqK,WAAY/H,EAAMwG,MAAM9I,OACxBwK,gBAAiBA,EACjBG,QAAoC,UAA3BrI,EAAMc,QAAQC,UAGgB,MAArCf,EAAMmG,cAAcD,gBACtBlG,EAAMK,OAAO3C,OAASrB,OAAOkE,OAAO,CAAC,EAAGP,EAAMK,OAAO3C,OAAQmK,GAAYxL,OAAOkE,OAAO,CAAC,EAAG0I,EAAc,CACvGhB,QAASjI,EAAMmG,cAAcD,cAC7BrF,SAAUb,EAAMc,QAAQC,SACxBoH,SAAUA,EACVC,aAAcA,OAIe,MAA7BpI,EAAMmG,cAAcjF,QACtBlB,EAAMK,OAAOa,MAAQ7E,OAAOkE,OAAO,CAAC,EAAGP,EAAMK,OAAOa,MAAO2G,GAAYxL,OAAOkE,OAAO,CAAC,EAAG0I,EAAc,CACrGhB,QAASjI,EAAMmG,cAAcjF,MAC7BL,SAAU,WACVsH,UAAU,EACVC,aAAcA,OAIlBpI,EAAMM,WAAW5C,OAASrB,OAAOkE,OAAO,CAAC,EAAGP,EAAMM,WAAW5C,OAAQ,CACnE,wBAAyBsC,EAAMjC,WAEnC,EAQE2L,KAAM,CAAC,GCrKT,IAAIC,GAAU,CACZA,SAAS,GAsCX,UACEhK,KAAM,iBACNC,SAAS,EACTC,MAAO,QACPC,GAAI,WAAe,EACnBY,OAxCF,SAAgBX,GACd,IAAIC,EAAQD,EAAKC,MACb4J,EAAW7J,EAAK6J,SAChB9I,EAAUf,EAAKe,QACf+I,EAAkB/I,EAAQgJ,OAC1BA,OAA6B,IAApBD,GAAoCA,EAC7CE,EAAkBjJ,EAAQkJ,OAC1BA,OAA6B,IAApBD,GAAoCA,EAC7C9K,EAASF,EAAUiB,EAAME,SAASxC,QAClCuM,EAAgB,GAAGjM,OAAOgC,EAAMiK,cAActM,UAAWqC,EAAMiK,cAAcvM,QAYjF,OAVIoM,GACFG,EAAc9J,SAAQ,SAAU+J,GAC9BA,EAAaC,iBAAiB,SAAUP,EAASQ,OAAQT,GAC3D,IAGEK,GACF/K,EAAOkL,iBAAiB,SAAUP,EAASQ,OAAQT,IAG9C,WACDG,GACFG,EAAc9J,SAAQ,SAAU+J,GAC9BA,EAAaG,oBAAoB,SAAUT,EAASQ,OAAQT,GAC9D,IAGEK,GACF/K,EAAOoL,oBAAoB,SAAUT,EAASQ,OAAQT,GAE1D,CACF,EASED,KAAM,CAAC,GC/CT,IAAIY,GAAO,CACTnN,KAAM,QACND,MAAO,OACPD,OAAQ,MACR+D,IAAK,UAEQ,SAASuJ,GAAqBxM,GAC3C,OAAOA,EAAUyM,QAAQ,0BAA0B,SAAUC,GAC3D,OAAOH,GAAKG,EACd,GACF,CCVA,IAAI,GAAO,CACTnN,MAAO,MACPC,IAAK,SAEQ,SAASmN,GAA8B3M,GACpD,OAAOA,EAAUyM,QAAQ,cAAc,SAAUC,GAC/C,OAAO,GAAKA,EACd,GACF,CCPe,SAASE,GAAgB3L,GACtC,IAAI6J,EAAM9J,EAAUC,GAGpB,MAAO,CACL4L,WAHe/B,EAAIgC,YAInBC,UAHcjC,EAAIkC,YAKtB,CCNe,SAASC,GAAoBpM,GAQ1C,OAAO+D,EAAsB8B,EAAmB7F,IAAUzB,KAAOwN,GAAgB/L,GAASgM,UAC5F,CCXe,SAASK,GAAerM,GAErC,IAAIsM,EAAoB,EAAiBtM,GACrCuM,EAAWD,EAAkBC,SAC7BC,EAAYF,EAAkBE,UAC9BC,EAAYH,EAAkBG,UAElC,MAAO,6BAA6B3I,KAAKyI,EAAWE,EAAYD,EAClE,CCLe,SAASE,GAAgBtM,GACtC,MAAI,CAAC,OAAQ,OAAQ,aAAawF,QAAQ7F,EAAYK,KAAU,EAEvDA,EAAKG,cAAcoM,KAGxBhM,EAAcP,IAASiM,GAAejM,GACjCA,EAGFsM,GAAgB1G,EAAc5F,GACvC,CCJe,SAASwM,GAAkB5M,EAAS6M,GACjD,IAAIC,OAES,IAATD,IACFA,EAAO,IAGT,IAAIvB,EAAeoB,GAAgB1M,GAC/B+M,EAASzB,KAAqE,OAAlDwB,EAAwB9M,EAAQO,oBAAyB,EAASuM,EAAsBH,MACpH1C,EAAM9J,EAAUmL,GAChB0B,EAASD,EAAS,CAAC9C,GAAK7K,OAAO6K,EAAIxF,gBAAkB,GAAI4H,GAAef,GAAgBA,EAAe,IAAMA,EAC7G2B,EAAcJ,EAAKzN,OAAO4N,GAC9B,OAAOD,EAASE,EAChBA,EAAY7N,OAAOwN,GAAkB5G,EAAcgH,IACrD,CCzBe,SAASE,GAAiBC,GACvC,OAAO1P,OAAOkE,OAAO,CAAC,EAAGwL,EAAM,CAC7B5O,KAAM4O,EAAKxI,EACXvC,IAAK+K,EAAKtI,EACVvG,MAAO6O,EAAKxI,EAAIwI,EAAK7I,MACrBjG,OAAQ8O,EAAKtI,EAAIsI,EAAK3I,QAE1B,CCqBA,SAAS4I,GAA2BpN,EAASqN,EAAgBlL,GAC3D,OAAOkL,IAAmBxO,EAAWqO,GCzBxB,SAAyBlN,EAASmC,GAC/C,IAAI8H,EAAM9J,EAAUH,GAChBsN,EAAOzH,EAAmB7F,GAC1ByE,EAAiBwF,EAAIxF,eACrBH,EAAQgJ,EAAKhF,YACb9D,EAAS8I,EAAKjF,aACd1D,EAAI,EACJE,EAAI,EAER,GAAIJ,EAAgB,CAClBH,EAAQG,EAAeH,MACvBE,EAASC,EAAeD,OACxB,IAAI+I,EAAiB1J,KAEjB0J,IAAmBA,GAA+B,UAAbpL,KACvCwC,EAAIF,EAAeG,WACnBC,EAAIJ,EAAeK,UAEvB,CAEA,MAAO,CACLR,MAAOA,EACPE,OAAQA,EACRG,EAAGA,EAAIyH,GAAoBpM,GAC3B6E,EAAGA,EAEP,CDDwD2I,CAAgBxN,EAASmC,IAAa1B,EAAU4M,GAdxG,SAAoCrN,EAASmC,GAC3C,IAAIgL,EAAOpJ,EAAsB/D,GAAS,EAAoB,UAAbmC,GASjD,OARAgL,EAAK/K,IAAM+K,EAAK/K,IAAMpC,EAAQyN,UAC9BN,EAAK5O,KAAO4O,EAAK5O,KAAOyB,EAAQ0N,WAChCP,EAAK9O,OAAS8O,EAAK/K,IAAMpC,EAAQqI,aACjC8E,EAAK7O,MAAQ6O,EAAK5O,KAAOyB,EAAQsI,YACjC6E,EAAK7I,MAAQtE,EAAQsI,YACrB6E,EAAK3I,OAASxE,EAAQqI,aACtB8E,EAAKxI,EAAIwI,EAAK5O,KACd4O,EAAKtI,EAAIsI,EAAK/K,IACP+K,CACT,CAG0HQ,CAA2BN,EAAgBlL,GAAY+K,GEtBlK,SAAyBlN,GACtC,IAAI8M,EAEAQ,EAAOzH,EAAmB7F,GAC1B4N,EAAY7B,GAAgB/L,GAC5B2M,EAA0D,OAAlDG,EAAwB9M,EAAQO,oBAAyB,EAASuM,EAAsBH,KAChGrI,EAAQ,EAAIgJ,EAAKO,YAAaP,EAAKhF,YAAaqE,EAAOA,EAAKkB,YAAc,EAAGlB,EAAOA,EAAKrE,YAAc,GACvG9D,EAAS,EAAI8I,EAAKQ,aAAcR,EAAKjF,aAAcsE,EAAOA,EAAKmB,aAAe,EAAGnB,EAAOA,EAAKtE,aAAe,GAC5G1D,GAAKiJ,EAAU5B,WAAaI,GAAoBpM,GAChD6E,GAAK+I,EAAU1B,UAMnB,MAJiD,QAA7C,EAAiBS,GAAQW,GAAMS,YACjCpJ,GAAK,EAAI2I,EAAKhF,YAAaqE,EAAOA,EAAKrE,YAAc,GAAKhE,GAGrD,CACLA,MAAOA,EACPE,OAAQA,EACRG,EAAGA,EACHE,EAAGA,EAEP,CFCkMmJ,CAAgBnI,EAAmB7F,IACrO,CG1Be,SAASiO,GAAe9M,GACrC,IAOIkI,EAPAtK,EAAYoC,EAAKpC,UACjBiB,EAAUmB,EAAKnB,QACfb,EAAYgC,EAAKhC,UACjBqI,EAAgBrI,EAAYuD,EAAiBvD,GAAa,KAC1DiK,EAAYjK,EAAY4J,EAAa5J,GAAa,KAClD+O,EAAUnP,EAAU4F,EAAI5F,EAAUuF,MAAQ,EAAItE,EAAQsE,MAAQ,EAC9D6J,EAAUpP,EAAU8F,EAAI9F,EAAUyF,OAAS,EAAIxE,EAAQwE,OAAS,EAGpE,OAAQgD,GACN,KAAK,EACH6B,EAAU,CACR1E,EAAGuJ,EACHrJ,EAAG9F,EAAU8F,EAAI7E,EAAQwE,QAE3B,MAEF,KAAKnG,EACHgL,EAAU,CACR1E,EAAGuJ,EACHrJ,EAAG9F,EAAU8F,EAAI9F,EAAUyF,QAE7B,MAEF,KAAKlG,EACH+K,EAAU,CACR1E,EAAG5F,EAAU4F,EAAI5F,EAAUuF,MAC3BO,EAAGsJ,GAEL,MAEF,KAAK5P,EACH8K,EAAU,CACR1E,EAAG5F,EAAU4F,EAAI3E,EAAQsE,MACzBO,EAAGsJ,GAEL,MAEF,QACE9E,EAAU,CACR1E,EAAG5F,EAAU4F,EACbE,EAAG9F,EAAU8F,GAInB,IAAIuJ,EAAW5G,EAAgBV,EAAyBU,GAAiB,KAEzE,GAAgB,MAAZ4G,EAAkB,CACpB,IAAI1G,EAAmB,MAAb0G,EAAmB,SAAW,QAExC,OAAQhF,GACN,KAAK1K,EACH2K,EAAQ+E,GAAY/E,EAAQ+E,IAAarP,EAAU2I,GAAO,EAAI1H,EAAQ0H,GAAO,GAC7E,MAEF,KAAK/I,EACH0K,EAAQ+E,GAAY/E,EAAQ+E,IAAarP,EAAU2I,GAAO,EAAI1H,EAAQ0H,GAAO,GAKnF,CAEA,OAAO2B,CACT,CC3De,SAASgF,GAAejN,EAAOc,QAC5B,IAAZA,IACFA,EAAU,CAAC,GAGb,IAAIoM,EAAWpM,EACXqM,EAAqBD,EAASnP,UAC9BA,OAAmC,IAAvBoP,EAAgCnN,EAAMjC,UAAYoP,EAC9DC,EAAoBF,EAASnM,SAC7BA,OAAiC,IAAtBqM,EAA+BpN,EAAMe,SAAWqM,EAC3DC,EAAoBH,EAASI,SAC7BA,OAAiC,IAAtBD,EAA+B7P,EAAkB6P,EAC5DE,EAAwBL,EAASM,aACjCA,OAAyC,IAA1BD,EAAmC9P,EAAW8P,EAC7DE,EAAwBP,EAASQ,eACjCA,OAA2C,IAA1BD,EAAmC/P,EAAS+P,EAC7DE,EAAuBT,EAASU,YAChCA,OAAuC,IAAzBD,GAA0CA,EACxDE,EAAmBX,EAAS3G,QAC5BA,OAA+B,IAArBsH,EAA8B,EAAIA,EAC5ChI,EAAgBD,EAAsC,iBAAZW,EAAuBA,EAAUT,EAAgBS,EAASlJ,IACpGyQ,EAAaJ,IAAmBhQ,EAASC,EAAYD,EACrDqK,EAAa/H,EAAMwG,MAAM9I,OACzBkB,EAAUoB,EAAME,SAAS0N,EAAcE,EAAaJ,GACpDK,EJkBS,SAAyBnP,EAAS0O,EAAUE,EAAczM,GACvE,IAAIiN,EAAmC,oBAAbV,EAlB5B,SAA4B1O,GAC1B,IAAIpB,EAAkBgO,GAAkB5G,EAAchG,IAElDqP,EADoB,CAAC,WAAY,SAASzJ,QAAQ,EAAiB5F,GAASiC,WAAa,GACnDtB,EAAcX,GAAWoG,EAAgBpG,GAAWA,EAE9F,OAAKS,EAAU4O,GAKRzQ,EAAgBgI,QAAO,SAAUyG,GACtC,OAAO5M,EAAU4M,IAAmBpI,EAASoI,EAAgBgC,IAAmD,SAAhCtP,EAAYsN,EAC9F,IANS,EAOX,CAK6DiC,CAAmBtP,GAAW,GAAGZ,OAAOsP,GAC/F9P,EAAkB,GAAGQ,OAAOgQ,EAAqB,CAACR,IAClDW,EAAsB3Q,EAAgB,GACtC4Q,EAAe5Q,EAAgBK,QAAO,SAAUwQ,EAASpC,GAC3D,IAAIF,EAAOC,GAA2BpN,EAASqN,EAAgBlL,GAK/D,OAJAsN,EAAQrN,IAAM,EAAI+K,EAAK/K,IAAKqN,EAAQrN,KACpCqN,EAAQnR,MAAQ,EAAI6O,EAAK7O,MAAOmR,EAAQnR,OACxCmR,EAAQpR,OAAS,EAAI8O,EAAK9O,OAAQoR,EAAQpR,QAC1CoR,EAAQlR,KAAO,EAAI4O,EAAK5O,KAAMkR,EAAQlR,MAC/BkR,CACT,GAAGrC,GAA2BpN,EAASuP,EAAqBpN,IAK5D,OAJAqN,EAAalL,MAAQkL,EAAalR,MAAQkR,EAAajR,KACvDiR,EAAahL,OAASgL,EAAanR,OAASmR,EAAapN,IACzDoN,EAAa7K,EAAI6K,EAAajR,KAC9BiR,EAAa3K,EAAI2K,EAAapN,IACvBoN,CACT,CInC2BE,CAAgBjP,EAAUT,GAAWA,EAAUA,EAAQ2P,gBAAkB9J,EAAmBzE,EAAME,SAASxC,QAAS4P,EAAUE,EAAczM,GACjKyN,EAAsB7L,EAAsB3C,EAAME,SAASvC,WAC3DuI,EAAgB2G,GAAe,CACjClP,UAAW6Q,EACX5P,QAASmJ,EACThH,SAAU,WACVhD,UAAWA,IAET0Q,EAAmB3C,GAAiBzP,OAAOkE,OAAO,CAAC,EAAGwH,EAAY7B,IAClEwI,EAAoBhB,IAAmBhQ,EAAS+Q,EAAmBD,EAGnEG,EAAkB,CACpB3N,IAAK+M,EAAmB/M,IAAM0N,EAAkB1N,IAAM6E,EAAc7E,IACpE/D,OAAQyR,EAAkBzR,OAAS8Q,EAAmB9Q,OAAS4I,EAAc5I,OAC7EE,KAAM4Q,EAAmB5Q,KAAOuR,EAAkBvR,KAAO0I,EAAc1I,KACvED,MAAOwR,EAAkBxR,MAAQ6Q,EAAmB7Q,MAAQ2I,EAAc3I,OAExE0R,EAAa5O,EAAMmG,cAAckB,OAErC,GAAIqG,IAAmBhQ,GAAUkR,EAAY,CAC3C,IAAIvH,EAASuH,EAAW7Q,GACxB1B,OAAO4D,KAAK0O,GAAiBxO,SAAQ,SAAUhE,GAC7C,IAAI0S,EAAW,CAAC3R,EAAOD,GAAQuH,QAAQrI,IAAQ,EAAI,GAAK,EACpDkK,EAAO,CAAC,EAAKpJ,GAAQuH,QAAQrI,IAAQ,EAAI,IAAM,IACnDwS,EAAgBxS,IAAQkL,EAAOhB,GAAQwI,CACzC,GACF,CAEA,OAAOF,CACT,CCyEA,UACEhP,KAAM,OACNC,SAAS,EACTC,MAAO,OACPC,GA5HF,SAAcC,GACZ,IAAIC,EAAQD,EAAKC,MACbc,EAAUf,EAAKe,QACfnB,EAAOI,EAAKJ,KAEhB,IAAIK,EAAMmG,cAAcxG,GAAMmP,MAA9B,CAoCA,IAhCA,IAAIC,EAAoBjO,EAAQkM,SAC5BgC,OAAsC,IAAtBD,GAAsCA,EACtDE,EAAmBnO,EAAQoO,QAC3BC,OAAoC,IAArBF,GAAqCA,EACpDG,EAA8BtO,EAAQuO,mBACtC9I,EAAUzF,EAAQyF,QAClB+G,EAAWxM,EAAQwM,SACnBE,EAAe1M,EAAQ0M,aACvBI,EAAc9M,EAAQ8M,YACtB0B,EAAwBxO,EAAQyO,eAChCA,OAA2C,IAA1BD,GAA0CA,EAC3DE,EAAwB1O,EAAQ0O,sBAChCC,EAAqBzP,EAAMc,QAAQ/C,UACnCqI,EAAgB9E,EAAiBmO,GAEjCJ,EAAqBD,IADHhJ,IAAkBqJ,GACqCF,EAjC/E,SAAuCxR,GACrC,GAAIuD,EAAiBvD,KAAeX,EAClC,MAAO,GAGT,IAAIsS,EAAoBnF,GAAqBxM,GAC7C,MAAO,CAAC2M,GAA8B3M,GAAY2R,EAAmBhF,GAA8BgF,GACrG,CA0B6IC,CAA8BF,GAA3E,CAAClF,GAAqBkF,KAChHG,EAAa,CAACH,GAAoBzR,OAAOqR,GAAoBxR,QAAO,SAAUC,EAAKC,GACrF,OAAOD,EAAIE,OAAOsD,EAAiBvD,KAAeX,ECvCvC,SAA8B4C,EAAOc,QAClC,IAAZA,IACFA,EAAU,CAAC,GAGb,IAAIoM,EAAWpM,EACX/C,EAAYmP,EAASnP,UACrBuP,EAAWJ,EAASI,SACpBE,EAAeN,EAASM,aACxBjH,EAAU2G,EAAS3G,QACnBgJ,EAAiBrC,EAASqC,eAC1BM,EAAwB3C,EAASsC,sBACjCA,OAAkD,IAA1BK,EAAmC,EAAgBA,EAC3E7H,EAAYL,EAAa5J,GACzB6R,EAAa5H,EAAYuH,EAAiB3R,EAAsBA,EAAoB4H,QAAO,SAAUzH,GACvG,OAAO4J,EAAa5J,KAAeiK,CACrC,IAAK3K,EACDyS,EAAoBF,EAAWpK,QAAO,SAAUzH,GAClD,OAAOyR,EAAsBhL,QAAQzG,IAAc,CACrD,IAEiC,IAA7B+R,EAAkBC,SACpBD,EAAoBF,GAItB,IAAII,EAAYF,EAAkBjS,QAAO,SAAUC,EAAKC,GAOtD,OANAD,EAAIC,GAAakP,GAAejN,EAAO,CACrCjC,UAAWA,EACXuP,SAAUA,EACVE,aAAcA,EACdjH,QAASA,IACRjF,EAAiBvD,IACbD,CACT,GAAG,CAAC,GACJ,OAAOzB,OAAO4D,KAAK+P,GAAWC,MAAK,SAAUC,EAAGC,GAC9C,OAAOH,EAAUE,GAAKF,EAAUG,EAClC,GACF,CDC6DC,CAAqBpQ,EAAO,CACnFjC,UAAWA,EACXuP,SAAUA,EACVE,aAAcA,EACdjH,QAASA,EACTgJ,eAAgBA,EAChBC,sBAAuBA,IACpBzR,EACP,GAAG,IACCsS,EAAgBrQ,EAAMwG,MAAM7I,UAC5BoK,EAAa/H,EAAMwG,MAAM9I,OACzB4S,EAAY,IAAIC,IAChBC,GAAqB,EACrBC,EAAwBb,EAAW,GAE9Bc,EAAI,EAAGA,EAAId,EAAWG,OAAQW,IAAK,CAC1C,IAAI3S,EAAY6R,EAAWc,GAEvBC,EAAiBrP,EAAiBvD,GAElC6S,EAAmBjJ,EAAa5J,KAAeT,EAC/CuT,EAAa,CAAC,EAAK5T,GAAQuH,QAAQmM,IAAmB,EACtDrK,EAAMuK,EAAa,QAAU,SAC7B1F,EAAW8B,GAAejN,EAAO,CACnCjC,UAAWA,EACXuP,SAAUA,EACVE,aAAcA,EACdI,YAAaA,EACbrH,QAASA,IAEPuK,EAAoBD,EAAaD,EAAmB1T,EAAQC,EAAOyT,EAAmB3T,EAAS,EAE/FoT,EAAc/J,GAAOyB,EAAWzB,KAClCwK,EAAoBvG,GAAqBuG,IAG3C,IAAIC,EAAmBxG,GAAqBuG,GACxCE,EAAS,GAUb,GARIhC,GACFgC,EAAOC,KAAK9F,EAASwF,IAAmB,GAGtCxB,GACF6B,EAAOC,KAAK9F,EAAS2F,IAAsB,EAAG3F,EAAS4F,IAAqB,GAG1EC,EAAOE,OAAM,SAAUC,GACzB,OAAOA,CACT,IAAI,CACFV,EAAwB1S,EACxByS,GAAqB,EACrB,KACF,CAEAF,EAAUc,IAAIrT,EAAWiT,EAC3B,CAEA,GAAIR,EAqBF,IAnBA,IAEIa,EAAQ,SAAeC,GACzB,IAAIC,EAAmB3B,EAAW4B,MAAK,SAAUzT,GAC/C,IAAIiT,EAASV,EAAU9T,IAAIuB,GAE3B,GAAIiT,EACF,OAAOA,EAAOS,MAAM,EAAGH,GAAIJ,OAAM,SAAUC,GACzC,OAAOA,CACT,GAEJ,IAEA,GAAII,EAEF,OADAd,EAAwBc,EACjB,OAEX,EAESD,EAnBY/B,EAAiB,EAAI,EAmBZ+B,EAAK,GAGpB,UAFFD,EAAMC,GADmBA,KAOpCtR,EAAMjC,YAAc0S,IACtBzQ,EAAMmG,cAAcxG,GAAMmP,OAAQ,EAClC9O,EAAMjC,UAAY0S,EAClBzQ,EAAM0R,OAAQ,EA5GhB,CA8GF,EAQEhK,iBAAkB,CAAC,UACnBgC,KAAM,CACJoF,OAAO,IE7IX,SAAS6C,GAAexG,EAAUY,EAAM6F,GAQtC,YAPyB,IAArBA,IACFA,EAAmB,CACjBrO,EAAG,EACHE,EAAG,IAIA,CACLzC,IAAKmK,EAASnK,IAAM+K,EAAK3I,OAASwO,EAAiBnO,EACnDvG,MAAOiO,EAASjO,MAAQ6O,EAAK7I,MAAQ0O,EAAiBrO,EACtDtG,OAAQkO,EAASlO,OAAS8O,EAAK3I,OAASwO,EAAiBnO,EACzDtG,KAAMgO,EAAShO,KAAO4O,EAAK7I,MAAQ0O,EAAiBrO,EAExD,CAEA,SAASsO,GAAsB1G,GAC7B,MAAO,CAAC,EAAKjO,EAAOD,EAAQE,GAAM2U,MAAK,SAAUC,GAC/C,OAAO5G,EAAS4G,IAAS,CAC3B,GACF,CA+BA,UACEpS,KAAM,OACNC,SAAS,EACTC,MAAO,OACP6H,iBAAkB,CAAC,mBACnB5H,GAlCF,SAAcC,GACZ,IAAIC,EAAQD,EAAKC,MACbL,EAAOI,EAAKJ,KACZ0Q,EAAgBrQ,EAAMwG,MAAM7I,UAC5BoK,EAAa/H,EAAMwG,MAAM9I,OACzBkU,EAAmB5R,EAAMmG,cAAc6L,gBACvCC,EAAoBhF,GAAejN,EAAO,CAC5C0N,eAAgB,cAEdwE,EAAoBjF,GAAejN,EAAO,CAC5C4N,aAAa,IAEXuE,EAA2BR,GAAeM,EAAmB5B,GAC7D+B,EAAsBT,GAAeO,EAAmBnK,EAAY6J,GACpES,EAAoBR,GAAsBM,GAC1CG,EAAmBT,GAAsBO,GAC7CpS,EAAMmG,cAAcxG,GAAQ,CAC1BwS,yBAA0BA,EAC1BC,oBAAqBA,EACrBC,kBAAmBA,EACnBC,iBAAkBA,GAEpBtS,EAAMM,WAAW5C,OAASrB,OAAOkE,OAAO,CAAC,EAAGP,EAAMM,WAAW5C,OAAQ,CACnE,+BAAgC2U,EAChC,sBAAuBC,GAE3B,GCJA,IACE3S,KAAM,SACNC,SAAS,EACTC,MAAO,OACPwB,SAAU,CAAC,iBACXvB,GA5BF,SAAgBa,GACd,IAAIX,EAAQW,EAAMX,MACdc,EAAUH,EAAMG,QAChBnB,EAAOgB,EAAMhB,KACb4S,EAAkBzR,EAAQuG,OAC1BA,OAA6B,IAApBkL,EAA6B,CAAC,EAAG,GAAKA,EAC/C7I,EAAO,EAAW7L,QAAO,SAAUC,EAAKC,GAE1C,OADAD,EAAIC,GA5BD,SAAiCA,EAAWyI,EAAOa,GACxD,IAAIjB,EAAgB9E,EAAiBvD,GACjCyU,EAAiB,CAACrV,EAAM,GAAKqH,QAAQ4B,IAAkB,GAAK,EAAI,EAEhErG,EAAyB,mBAAXsH,EAAwBA,EAAOhL,OAAOkE,OAAO,CAAC,EAAGiG,EAAO,CACxEzI,UAAWA,KACPsJ,EACFoL,EAAW1S,EAAK,GAChB2S,EAAW3S,EAAK,GAIpB,OAFA0S,EAAWA,GAAY,EACvBC,GAAYA,GAAY,GAAKF,EACtB,CAACrV,EAAMD,GAAOsH,QAAQ4B,IAAkB,EAAI,CACjD7C,EAAGmP,EACHjP,EAAGgP,GACD,CACFlP,EAAGkP,EACHhP,EAAGiP,EAEP,CASqBC,CAAwB5U,EAAWiC,EAAMwG,MAAOa,GAC1DvJ,CACT,GAAG,CAAC,GACA8U,EAAwBlJ,EAAK1J,EAAMjC,WACnCwF,EAAIqP,EAAsBrP,EAC1BE,EAAImP,EAAsBnP,EAEW,MAArCzD,EAAMmG,cAAcD,gBACtBlG,EAAMmG,cAAcD,cAAc3C,GAAKA,EACvCvD,EAAMmG,cAAcD,cAAczC,GAAKA,GAGzCzD,EAAMmG,cAAcxG,GAAQ+J,CAC9B,GC1BA,IACE/J,KAAM,gBACNC,SAAS,EACTC,MAAO,OACPC,GApBF,SAAuBC,GACrB,IAAIC,EAAQD,EAAKC,MACbL,EAAOI,EAAKJ,KAKhBK,EAAMmG,cAAcxG,GAAQkN,GAAe,CACzClP,UAAWqC,EAAMwG,MAAM7I,UACvBiB,QAASoB,EAAMwG,MAAM9I,OACrBqD,SAAU,WACVhD,UAAWiC,EAAMjC,WAErB,EAQE2L,KAAM,CAAC,GCgHT,IACE/J,KAAM,kBACNC,SAAS,EACTC,MAAO,OACPC,GA/HF,SAAyBC,GACvB,IAAIC,EAAQD,EAAKC,MACbc,EAAUf,EAAKe,QACfnB,EAAOI,EAAKJ,KACZoP,EAAoBjO,EAAQkM,SAC5BgC,OAAsC,IAAtBD,GAAsCA,EACtDE,EAAmBnO,EAAQoO,QAC3BC,OAAoC,IAArBF,GAAsCA,EACrD3B,EAAWxM,EAAQwM,SACnBE,EAAe1M,EAAQ0M,aACvBI,EAAc9M,EAAQ8M,YACtBrH,EAAUzF,EAAQyF,QAClBsM,EAAkB/R,EAAQgS,OAC1BA,OAA6B,IAApBD,GAAoCA,EAC7CE,EAAwBjS,EAAQkS,aAChCA,OAAyC,IAA1BD,EAAmC,EAAIA,EACtD5H,EAAW8B,GAAejN,EAAO,CACnCsN,SAAUA,EACVE,aAAcA,EACdjH,QAASA,EACTqH,YAAaA,IAEXxH,EAAgB9E,EAAiBtB,EAAMjC,WACvCiK,EAAYL,EAAa3H,EAAMjC,WAC/BkV,GAAmBjL,EACnBgF,EAAWtH,EAAyBU,GACpC8I,ECrCY,MDqCSlC,ECrCH,IAAM,IDsCxB9G,EAAgBlG,EAAMmG,cAAcD,cACpCmK,EAAgBrQ,EAAMwG,MAAM7I,UAC5BoK,EAAa/H,EAAMwG,MAAM9I,OACzBwV,EAA4C,mBAAjBF,EAA8BA,EAAa3W,OAAOkE,OAAO,CAAC,EAAGP,EAAMwG,MAAO,CACvGzI,UAAWiC,EAAMjC,aACbiV,EACFG,EAA2D,iBAAtBD,EAAiC,CACxElG,SAAUkG,EACVhE,QAASgE,GACP7W,OAAOkE,OAAO,CAChByM,SAAU,EACVkC,QAAS,GACRgE,GACCE,EAAsBpT,EAAMmG,cAAckB,OAASrH,EAAMmG,cAAckB,OAAOrH,EAAMjC,WAAa,KACjG2L,EAAO,CACTnG,EAAG,EACHE,EAAG,GAGL,GAAKyC,EAAL,CAIA,GAAI8I,EAAe,CACjB,IAAIqE,EAEAC,EAAwB,MAAbtG,EAAmB,EAAM7P,EACpCoW,EAAuB,MAAbvG,EAAmB/P,EAASC,EACtCoJ,EAAmB,MAAb0G,EAAmB,SAAW,QACpC3F,EAASnB,EAAc8G,GACvBtL,EAAM2F,EAAS8D,EAASmI,GACxB7R,EAAM4F,EAAS8D,EAASoI,GACxBC,EAAWV,GAAU/K,EAAWzB,GAAO,EAAI,EAC3CmN,EAASzL,IAAc1K,EAAQ+S,EAAc/J,GAAOyB,EAAWzB,GAC/DoN,EAAS1L,IAAc1K,GAASyK,EAAWzB,IAAQ+J,EAAc/J,GAGjEL,EAAejG,EAAME,SAASgB,MAC9BwF,EAAYoM,GAAU7M,EAAetC,EAAcsC,GAAgB,CACrE/C,MAAO,EACPE,OAAQ,GAENuQ,GAAqB3T,EAAMmG,cAAc,oBAAsBnG,EAAMmG,cAAc,oBAAoBI,QxBhFtG,CACLvF,IAAK,EACL9D,MAAO,EACPD,OAAQ,EACRE,KAAM,GwB6EFyW,GAAkBD,GAAmBL,GACrCO,GAAkBF,GAAmBJ,GAMrCO,GAAWnO,EAAO,EAAG0K,EAAc/J,GAAMI,EAAUJ,IACnDyN,GAAYd,EAAkB5C,EAAc/J,GAAO,EAAIkN,EAAWM,GAAWF,GAAkBT,EAA4BnG,SAAWyG,EAASK,GAAWF,GAAkBT,EAA4BnG,SACxMgH,GAAYf,GAAmB5C,EAAc/J,GAAO,EAAIkN,EAAWM,GAAWD,GAAkBV,EAA4BnG,SAAW0G,EAASI,GAAWD,GAAkBV,EAA4BnG,SACzMjG,GAAoB/G,EAAME,SAASgB,OAAS8D,EAAgBhF,EAAME,SAASgB,OAC3E+S,GAAelN,GAAiC,MAAbiG,EAAmBjG,GAAkBsF,WAAa,EAAItF,GAAkBuF,YAAc,EAAI,EAC7H4H,GAAwH,OAAjGb,EAA+C,MAAvBD,OAA8B,EAASA,EAAoBpG,IAAqBqG,EAAwB,EAEvJc,GAAY9M,EAAS2M,GAAYE,GACjCE,GAAkBzO,EAAOmN,EAAS,EAAQpR,EAF9B2F,EAAS0M,GAAYG,GAAsBD,IAEKvS,EAAK2F,EAAQyL,EAAS,EAAQrR,EAAK0S,IAAa1S,GAChHyE,EAAc8G,GAAYoH,GAC1B1K,EAAKsD,GAAYoH,GAAkB/M,CACrC,CAEA,GAAI8H,EAAc,CAChB,IAAIkF,GAEAC,GAAyB,MAAbtH,EAAmB,EAAM7P,EAErCoX,GAAwB,MAAbvH,EAAmB/P,EAASC,EAEvCsX,GAAUtO,EAAcgJ,GAExBuF,GAAmB,MAAZvF,EAAkB,SAAW,QAEpCwF,GAAOF,GAAUrJ,EAASmJ,IAE1BK,GAAOH,GAAUrJ,EAASoJ,IAE1BK,IAAuD,IAAxC,CAAC,EAAKzX,GAAMqH,QAAQ4B,GAEnCyO,GAAyH,OAAjGR,GAAgD,MAAvBjB,OAA8B,EAASA,EAAoBlE,IAAoBmF,GAAyB,EAEzJS,GAAaF,GAAeF,GAAOF,GAAUnE,EAAcoE,IAAQ1M,EAAW0M,IAAQI,GAAuB1B,EAA4BjE,QAEzI6F,GAAaH,GAAeJ,GAAUnE,EAAcoE,IAAQ1M,EAAW0M,IAAQI,GAAuB1B,EAA4BjE,QAAUyF,GAE5IK,GAAmBlC,GAAU8B,G1BzH9B,SAAwBlT,EAAK1E,EAAOyE,GACzC,IAAIwT,EAAItP,EAAOjE,EAAK1E,EAAOyE,GAC3B,OAAOwT,EAAIxT,EAAMA,EAAMwT,CACzB,C0BsHoDC,CAAeJ,GAAYN,GAASO,IAAcpP,EAAOmN,EAASgC,GAAaJ,GAAMF,GAAS1B,EAASiC,GAAaJ,IAEpKzO,EAAcgJ,GAAW8F,GACzBtL,EAAKwF,GAAW8F,GAAmBR,EACrC,CAEAxU,EAAMmG,cAAcxG,GAAQ+J,CAvE5B,CAwEF,EAQEhC,iBAAkB,CAAC,WE1HN,SAASyN,GAAiBC,EAAyBrQ,EAAcsD,QAC9D,IAAZA,IACFA,GAAU,GAGZ,ICnBoCrJ,ECJOJ,EFuBvCyW,EAA0B9V,EAAcwF,GACxCuQ,EAAuB/V,EAAcwF,IAf3C,SAAyBnG,GACvB,IAAImN,EAAOnN,EAAQ+D,wBACfI,EAASpB,EAAMoK,EAAK7I,OAAStE,EAAQqE,aAAe,EACpDD,EAASrB,EAAMoK,EAAK3I,QAAUxE,EAAQuE,cAAgB,EAC1D,OAAkB,IAAXJ,GAA2B,IAAXC,CACzB,CAU4DuS,CAAgBxQ,GACtEJ,EAAkBF,EAAmBM,GACrCgH,EAAOpJ,EAAsByS,EAAyBE,EAAsBjN,GAC5EyB,EAAS,CACXc,WAAY,EACZE,UAAW,GAET7C,EAAU,CACZ1E,EAAG,EACHE,EAAG,GAkBL,OAfI4R,IAA4BA,IAA4BhN,MACxB,SAA9B1J,EAAYoG,IAChBkG,GAAetG,MACbmF,GCnCgC9K,EDmCT+F,KClCdhG,EAAUC,IAAUO,EAAcP,GCJxC,CACL4L,YAFyChM,EDQbI,GCNR4L,WACpBE,UAAWlM,EAAQkM,WDGZH,GAAgB3L,IDoCnBO,EAAcwF,KAChBkD,EAAUtF,EAAsBoC,GAAc,IACtCxB,GAAKwB,EAAauH,WAC1BrE,EAAQxE,GAAKsB,EAAasH,WACjB1H,IACTsD,EAAQ1E,EAAIyH,GAAoBrG,KAI7B,CACLpB,EAAGwI,EAAK5O,KAAO2M,EAAOc,WAAa3C,EAAQ1E,EAC3CE,EAAGsI,EAAK/K,IAAM8I,EAAOgB,UAAY7C,EAAQxE,EACzCP,MAAO6I,EAAK7I,MACZE,OAAQ2I,EAAK3I,OAEjB,CGvDA,SAASoS,GAAMC,GACb,IAAItT,EAAM,IAAIoO,IACVmF,EAAU,IAAIC,IACdC,EAAS,GAKb,SAAS3F,EAAK4F,GACZH,EAAQI,IAAID,EAASlW,MACN,GAAG3B,OAAO6X,EAASxU,UAAY,GAAIwU,EAASnO,kBAAoB,IACtEvH,SAAQ,SAAU4V,GACzB,IAAKL,EAAQM,IAAID,GAAM,CACrB,IAAIE,EAAc9T,EAAI3F,IAAIuZ,GAEtBE,GACFhG,EAAKgG,EAET,CACF,IACAL,EAAO3E,KAAK4E,EACd,CAQA,OAzBAJ,EAAUtV,SAAQ,SAAU0V,GAC1B1T,EAAIiP,IAAIyE,EAASlW,KAAMkW,EACzB,IAiBAJ,EAAUtV,SAAQ,SAAU0V,GACrBH,EAAQM,IAAIH,EAASlW,OAExBsQ,EAAK4F,EAET,IACOD,CACT,CCvBA,IAAIM,GAAkB,CACpBnY,UAAW,SACX0X,UAAW,GACX1U,SAAU,YAGZ,SAASoV,KACP,IAAK,IAAI1B,EAAO2B,UAAUrG,OAAQsG,EAAO,IAAIpU,MAAMwS,GAAO6B,EAAO,EAAGA,EAAO7B,EAAM6B,IAC/ED,EAAKC,GAAQF,UAAUE,GAGzB,OAAQD,EAAKvE,MAAK,SAAUlT,GAC1B,QAASA,GAAoD,mBAAlCA,EAAQ+D,sBACrC,GACF,CAEO,SAAS4T,GAAgBC,QACL,IAArBA,IACFA,EAAmB,CAAC,GAGtB,IAAIC,EAAoBD,EACpBE,EAAwBD,EAAkBE,iBAC1CA,OAA6C,IAA1BD,EAAmC,GAAKA,EAC3DE,EAAyBH,EAAkBI,eAC3CA,OAA4C,IAA3BD,EAAoCV,GAAkBU,EAC3E,OAAO,SAAsBjZ,EAAWD,EAAQoD,QAC9B,IAAZA,IACFA,EAAU+V,GAGZ,ICxC6B/W,EAC3BgX,EDuCE9W,EAAQ,CACVjC,UAAW,SACXgZ,iBAAkB,GAClBjW,QAASzE,OAAOkE,OAAO,CAAC,EAAG2V,GAAiBW,GAC5C1Q,cAAe,CAAC,EAChBjG,SAAU,CACRvC,UAAWA,EACXD,OAAQA,GAEV4C,WAAY,CAAC,EACbD,OAAQ,CAAC,GAEP2W,EAAmB,GACnBC,GAAc,EACdrN,EAAW,CACb5J,MAAOA,EACPkX,WAAY,SAAoBC,GAC9B,IAAIrW,EAAsC,mBAArBqW,EAAkCA,EAAiBnX,EAAMc,SAAWqW,EACzFC,IACApX,EAAMc,QAAUzE,OAAOkE,OAAO,CAAC,EAAGsW,EAAgB7W,EAAMc,QAASA,GACjEd,EAAMiK,cAAgB,CACpBtM,UAAW0B,EAAU1B,GAAa6N,GAAkB7N,GAAaA,EAAU4Q,eAAiB/C,GAAkB7N,EAAU4Q,gBAAkB,GAC1I7Q,OAAQ8N,GAAkB9N,IAI5B,IElE4B+X,EAC9B4B,EFiEMN,EDhCG,SAAwBtB,GAErC,IAAIsB,EAAmBvB,GAAMC,GAE7B,OAAO/W,EAAeb,QAAO,SAAUC,EAAK+B,GAC1C,OAAO/B,EAAIE,OAAO+Y,EAAiBvR,QAAO,SAAUqQ,GAClD,OAAOA,EAAShW,QAAUA,CAC5B,IACF,GAAG,GACL,CCuB+ByX,EElEK7B,EFkEsB,GAAGzX,OAAO2Y,EAAkB3W,EAAMc,QAAQ2U,WEjE9F4B,EAAS5B,EAAU5X,QAAO,SAAUwZ,EAAQE,GAC9C,IAAIC,EAAWH,EAAOE,EAAQ5X,MAK9B,OAJA0X,EAAOE,EAAQ5X,MAAQ6X,EAAWnb,OAAOkE,OAAO,CAAC,EAAGiX,EAAUD,EAAS,CACrEzW,QAASzE,OAAOkE,OAAO,CAAC,EAAGiX,EAAS1W,QAASyW,EAAQzW,SACrD4I,KAAMrN,OAAOkE,OAAO,CAAC,EAAGiX,EAAS9N,KAAM6N,EAAQ7N,QAC5C6N,EACEF,CACT,GAAG,CAAC,GAEGhb,OAAO4D,KAAKoX,GAAQlV,KAAI,SAAUhG,GACvC,OAAOkb,EAAOlb,EAChB,MF4DM,OAJA6D,EAAM+W,iBAAmBA,EAAiBvR,QAAO,SAAUiS,GACzD,OAAOA,EAAE7X,OACX,IA+FFI,EAAM+W,iBAAiB5W,SAAQ,SAAUJ,GACvC,IAAIJ,EAAOI,EAAKJ,KACZ+X,EAAe3X,EAAKe,QACpBA,OAA2B,IAAjB4W,EAA0B,CAAC,EAAIA,EACzChX,EAASX,EAAKW,OAElB,GAAsB,mBAAXA,EAAuB,CAChC,IAAIiX,EAAYjX,EAAO,CACrBV,MAAOA,EACPL,KAAMA,EACNiK,SAAUA,EACV9I,QAASA,IAKXkW,EAAiB/F,KAAK0G,GAFT,WAAmB,EAGlC,CACF,IA/GS/N,EAASQ,QAClB,EAMAwN,YAAa,WACX,IAAIX,EAAJ,CAIA,IAAIY,EAAkB7X,EAAME,SACxBvC,EAAYka,EAAgBla,UAC5BD,EAASma,EAAgBna,OAG7B,GAAKyY,GAAiBxY,EAAWD,GAAjC,CAKAsC,EAAMwG,MAAQ,CACZ7I,UAAWwX,GAAiBxX,EAAWqH,EAAgBtH,GAAoC,UAA3BsC,EAAMc,QAAQC,UAC9ErD,OAAQiG,EAAcjG,IAOxBsC,EAAM0R,OAAQ,EACd1R,EAAMjC,UAAYiC,EAAMc,QAAQ/C,UAKhCiC,EAAM+W,iBAAiB5W,SAAQ,SAAU0V,GACvC,OAAO7V,EAAMmG,cAAc0P,EAASlW,MAAQtD,OAAOkE,OAAO,CAAC,EAAGsV,EAASnM,KACzE,IAEA,IAAK,IAAIoO,EAAQ,EAAGA,EAAQ9X,EAAM+W,iBAAiBhH,OAAQ+H,IACzD,IAAoB,IAAhB9X,EAAM0R,MAAV,CAMA,IAAIqG,EAAwB/X,EAAM+W,iBAAiBe,GAC/ChY,EAAKiY,EAAsBjY,GAC3BkY,EAAyBD,EAAsBjX,QAC/CoM,OAAsC,IAA3B8K,EAAoC,CAAC,EAAIA,EACpDrY,EAAOoY,EAAsBpY,KAEf,mBAAPG,IACTE,EAAQF,EAAG,CACTE,MAAOA,EACPc,QAASoM,EACTvN,KAAMA,EACNiK,SAAUA,KACN5J,EAdR,MAHEA,EAAM0R,OAAQ,EACdoG,GAAS,CAzBb,CATA,CAqDF,EAGA1N,QC1I2BtK,ED0IV,WACf,OAAO,IAAImY,SAAQ,SAAUC,GAC3BtO,EAASgO,cACTM,EAAQlY,EACV,GACF,EC7IG,WAUL,OATK8W,IACHA,EAAU,IAAImB,SAAQ,SAAUC,GAC9BD,QAAQC,UAAUC,MAAK,WACrBrB,OAAUsB,EACVF,EAAQpY,IACV,GACF,KAGKgX,CACT,GDmIIuB,QAAS,WACPjB,IACAH,GAAc,CAChB,GAGF,IAAKd,GAAiBxY,EAAWD,GAC/B,OAAOkM,EAmCT,SAASwN,IACPJ,EAAiB7W,SAAQ,SAAUL,GACjC,OAAOA,GACT,IACAkX,EAAmB,EACrB,CAEA,OAvCApN,EAASsN,WAAWpW,GAASqX,MAAK,SAAUnY,IACrCiX,GAAenW,EAAQwX,eAC1BxX,EAAQwX,cAActY,EAE1B,IAmCO4J,CACT,CACF,CACO,IAAI2O,GAA4BhC,KGzLnC,GAA4BA,GAAgB,CAC9CI,iBAFqB,CAAC6B,GAAgB,GAAe,GAAe,EAAa,GAAQ,GAAM,GAAiB,EAAO,MCJrH,GAA4BjC,GAAgB,CAC9CI,iBAFqB,CAAC6B,GAAgB,GAAe,GAAe,KCatE,MAAMC,GAAa,IAAIlI,IACjBmI,GAAO,CACX,GAAAtH,CAAIxS,EAASzC,EAAKyN,GACX6O,GAAWzC,IAAIpX,IAClB6Z,GAAWrH,IAAIxS,EAAS,IAAI2R,KAE9B,MAAMoI,EAAcF,GAAWjc,IAAIoC,GAI9B+Z,EAAY3C,IAAI7Z,IAA6B,IAArBwc,EAAYC,KAKzCD,EAAYvH,IAAIjV,EAAKyN,GAHnBiP,QAAQC,MAAM,+EAA+E7W,MAAM8W,KAAKJ,EAAY1Y,QAAQ,MAIhI,EACAzD,IAAG,CAACoC,EAASzC,IACPsc,GAAWzC,IAAIpX,IACV6Z,GAAWjc,IAAIoC,GAASpC,IAAIL,IAE9B,KAET,MAAA6c,CAAOpa,EAASzC,GACd,IAAKsc,GAAWzC,IAAIpX,GAClB,OAEF,MAAM+Z,EAAcF,GAAWjc,IAAIoC,GACnC+Z,EAAYM,OAAO9c,GAGM,IAArBwc,EAAYC,MACdH,GAAWQ,OAAOra,EAEtB,GAYIsa,GAAiB,gBAOjBC,GAAgBC,IAChBA,GAAYna,OAAOoa,KAAOpa,OAAOoa,IAAIC,SAEvCF,EAAWA,EAAS5O,QAAQ,iBAAiB,CAAC+O,EAAOC,IAAO,IAAIH,IAAIC,OAAOE,QAEtEJ,GA4CHK,GAAuB7a,IAC3BA,EAAQ8a,cAAc,IAAIC,MAAMT,IAAgB,EAE5C,GAAYU,MACXA,GAA4B,iBAAXA,UAGO,IAAlBA,EAAOC,SAChBD,EAASA,EAAO,SAEgB,IAApBA,EAAOE,UAEjBC,GAAaH,GAEb,GAAUA,GACLA,EAAOC,OAASD,EAAO,GAAKA,EAEf,iBAAXA,GAAuBA,EAAO7J,OAAS,EACzCrL,SAAS+C,cAAc0R,GAAcS,IAEvC,KAEHI,GAAYpb,IAChB,IAAK,GAAUA,IAAgD,IAApCA,EAAQqb,iBAAiBlK,OAClD,OAAO,EAET,MAAMmK,EAAgF,YAA7D5V,iBAAiB1F,GAASub,iBAAiB,cAE9DC,EAAgBxb,EAAQyb,QAAQ,uBACtC,IAAKD,EACH,OAAOF,EAET,GAAIE,IAAkBxb,EAAS,CAC7B,MAAM0b,EAAU1b,EAAQyb,QAAQ,WAChC,GAAIC,GAAWA,EAAQlW,aAAegW,EACpC,OAAO,EAET,GAAgB,OAAZE,EACF,OAAO,CAEX,CACA,OAAOJ,CAAgB,EAEnBK,GAAa3b,IACZA,GAAWA,EAAQkb,WAAaU,KAAKC,gBAGtC7b,EAAQ8b,UAAU7W,SAAS,mBAGC,IAArBjF,EAAQ+b,SACV/b,EAAQ+b,SAEV/b,EAAQgc,aAAa,aAAoD,UAArChc,EAAQic,aAAa,aAE5DC,GAAiBlc,IACrB,IAAK8F,SAASC,gBAAgBoW,aAC5B,OAAO,KAIT,GAAmC,mBAAxBnc,EAAQqF,YAA4B,CAC7C,MAAM+W,EAAOpc,EAAQqF,cACrB,OAAO+W,aAAgBtb,WAAasb,EAAO,IAC7C,CACA,OAAIpc,aAAmBc,WACdd,EAIJA,EAAQwF,WAGN0W,GAAelc,EAAQwF,YAFrB,IAEgC,EAErC6W,GAAO,OAUPC,GAAStc,IACbA,EAAQuE,YAAY,EAGhBgY,GAAY,IACZlc,OAAOmc,SAAW1W,SAAS6G,KAAKqP,aAAa,qBACxC3b,OAAOmc,OAET,KAEHC,GAA4B,GAgB5BC,GAAQ,IAAuC,QAAjC5W,SAASC,gBAAgB4W,IACvCC,GAAqBC,IAhBAC,QAiBN,KACjB,MAAMC,EAAIR,KAEV,GAAIQ,EAAG,CACL,MAAMhc,EAAO8b,EAAOG,KACdC,EAAqBF,EAAE7b,GAAGH,GAChCgc,EAAE7b,GAAGH,GAAQ8b,EAAOK,gBACpBH,EAAE7b,GAAGH,GAAMoc,YAAcN,EACzBE,EAAE7b,GAAGH,GAAMqc,WAAa,KACtBL,EAAE7b,GAAGH,GAAQkc,EACNJ,EAAOK,gBAElB,GA5B0B,YAAxBpX,SAASuX,YAENZ,GAA0BtL,QAC7BrL,SAASyF,iBAAiB,oBAAoB,KAC5C,IAAK,MAAMuR,KAAYL,GACrBK,GACF,IAGJL,GAA0BpK,KAAKyK,IAE/BA,GAkBA,EAEEQ,GAAU,CAACC,EAAkB9F,EAAO,GAAI+F,EAAeD,IACxB,mBAArBA,EAAkCA,KAAoB9F,GAAQ+F,EAExEC,GAAyB,CAACX,EAAUY,EAAmBC,GAAoB,KAC/E,IAAKA,EAEH,YADAL,GAAQR,GAGV,MACMc,EAhKiC5d,KACvC,IAAKA,EACH,OAAO,EAIT,IAAI,mBACF6d,EAAkB,gBAClBC,GACEzd,OAAOqF,iBAAiB1F,GAC5B,MAAM+d,EAA0BC,OAAOC,WAAWJ,GAC5CK,EAAuBF,OAAOC,WAAWH,GAG/C,OAAKC,GAA4BG,GAKjCL,EAAqBA,EAAmBlb,MAAM,KAAK,GACnDmb,EAAkBA,EAAgBnb,MAAM,KAAK,GAtDf,KAuDtBqb,OAAOC,WAAWJ,GAAsBG,OAAOC,WAAWH,KANzD,CAMoG,EA2IpFK,CAAiCT,GADlC,EAExB,IAAIU,GAAS,EACb,MAAMC,EAAU,EACdrR,aAEIA,IAAW0Q,IAGfU,GAAS,EACTV,EAAkBjS,oBAAoB6O,GAAgB+D,GACtDf,GAAQR,GAAS,EAEnBY,EAAkBnS,iBAAiB+O,GAAgB+D,GACnDC,YAAW,KACJF,GACHvD,GAAqB6C,EACvB,GACCE,EAAiB,EAYhBW,GAAuB,CAAC1R,EAAM2R,EAAeC,EAAeC,KAChE,MAAMC,EAAa9R,EAAKsE,OACxB,IAAI+H,EAAQrM,EAAKjH,QAAQ4Y,GAIzB,OAAe,IAAXtF,GACMuF,GAAiBC,EAAiB7R,EAAK8R,EAAa,GAAK9R,EAAK,IAExEqM,GAASuF,EAAgB,GAAK,EAC1BC,IACFxF,GAASA,EAAQyF,GAAcA,GAE1B9R,EAAKjK,KAAKC,IAAI,EAAGD,KAAKE,IAAIoW,EAAOyF,EAAa,KAAI,EAerDC,GAAiB,qBACjBC,GAAiB,OACjBC,GAAgB,SAChBC,GAAgB,CAAC,EACvB,IAAIC,GAAW,EACf,MAAMC,GAAe,CACnBC,WAAY,YACZC,WAAY,YAERC,GAAe,IAAIrI,IAAI,CAAC,QAAS,WAAY,UAAW,YAAa,cAAe,aAAc,iBAAkB,YAAa,WAAY,YAAa,cAAe,YAAa,UAAW,WAAY,QAAS,oBAAqB,aAAc,YAAa,WAAY,cAAe,cAAe,cAAe,YAAa,eAAgB,gBAAiB,eAAgB,gBAAiB,aAAc,QAAS,OAAQ,SAAU,QAAS,SAAU,SAAU,UAAW,WAAY,OAAQ,SAAU,eAAgB,SAAU,OAAQ,mBAAoB,mBAAoB,QAAS,QAAS,WAM/lB,SAASsI,GAAarf,EAASsf,GAC7B,OAAOA,GAAO,GAAGA,MAAQN,QAAgBhf,EAAQgf,UAAYA,IAC/D,CACA,SAASO,GAAiBvf,GACxB,MAAMsf,EAAMD,GAAarf,GAGzB,OAFAA,EAAQgf,SAAWM,EACnBP,GAAcO,GAAOP,GAAcO,IAAQ,CAAC,EACrCP,GAAcO,EACvB,CAiCA,SAASE,GAAYC,EAAQC,EAAUC,EAAqB,MAC1D,OAAOliB,OAAOmiB,OAAOH,GAAQ7M,MAAKiN,GAASA,EAAMH,WAAaA,GAAYG,EAAMF,qBAAuBA,GACzG,CACA,SAASG,GAAoBC,EAAmB1B,EAAS2B,GACvD,MAAMC,EAAiC,iBAAZ5B,EAErBqB,EAAWO,EAAcD,EAAqB3B,GAAW2B,EAC/D,IAAIE,EAAYC,GAAaJ,GAI7B,OAHKX,GAAahI,IAAI8I,KACpBA,EAAYH,GAEP,CAACE,EAAaP,EAAUQ,EACjC,CACA,SAASE,GAAWpgB,EAAS+f,EAAmB1B,EAAS2B,EAAoBK,GAC3E,GAAiC,iBAAtBN,IAAmC/f,EAC5C,OAEF,IAAKigB,EAAaP,EAAUQ,GAAaJ,GAAoBC,EAAmB1B,EAAS2B,GAIzF,GAAID,KAAqBd,GAAc,CACrC,MAAMqB,EAAepf,GACZ,SAAU2e,GACf,IAAKA,EAAMU,eAAiBV,EAAMU,gBAAkBV,EAAMW,iBAAmBX,EAAMW,eAAevb,SAAS4a,EAAMU,eAC/G,OAAOrf,EAAGjD,KAAKwiB,KAAMZ,EAEzB,EAEFH,EAAWY,EAAaZ,EAC1B,CACA,MAAMD,EAASF,GAAiBvf,GAC1B0gB,EAAWjB,EAAOS,KAAeT,EAAOS,GAAa,CAAC,GACtDS,EAAmBnB,GAAYkB,EAAUhB,EAAUO,EAAc5B,EAAU,MACjF,GAAIsC,EAEF,YADAA,EAAiBN,OAASM,EAAiBN,QAAUA,GAGvD,MAAMf,EAAMD,GAAaK,EAAUK,EAAkBnU,QAAQgT,GAAgB,KACvE1d,EAAK+e,EA5Db,SAAoCjgB,EAASwa,EAAUtZ,GACrD,OAAO,SAASmd,EAAQwB,GACtB,MAAMe,EAAc5gB,EAAQ6gB,iBAAiBrG,GAC7C,IAAK,IAAI,OACPxN,GACE6S,EAAO7S,GAAUA,IAAWyT,KAAMzT,EAASA,EAAOxH,WACpD,IAAK,MAAMsb,KAAcF,EACvB,GAAIE,IAAe9T,EASnB,OANA+T,GAAWlB,EAAO,CAChBW,eAAgBxT,IAEdqR,EAAQgC,QACVW,GAAaC,IAAIjhB,EAAS6f,EAAMqB,KAAM1G,EAAUtZ,GAE3CA,EAAGigB,MAAMnU,EAAQ,CAAC6S,GAG/B,CACF,CAwC2BuB,CAA2BphB,EAASqe,EAASqB,GAvExE,SAA0B1f,EAASkB,GACjC,OAAO,SAASmd,EAAQwB,GAOtB,OANAkB,GAAWlB,EAAO,CAChBW,eAAgBxgB,IAEdqe,EAAQgC,QACVW,GAAaC,IAAIjhB,EAAS6f,EAAMqB,KAAMhgB,GAEjCA,EAAGigB,MAAMnhB,EAAS,CAAC6f,GAC5B,CACF,CA6DoFwB,CAAiBrhB,EAAS0f,GAC5Gxe,EAAGye,mBAAqBM,EAAc5B,EAAU,KAChDnd,EAAGwe,SAAWA,EACdxe,EAAGmf,OAASA,EACZnf,EAAG8d,SAAWM,EACdoB,EAASpB,GAAOpe,EAChBlB,EAAQuL,iBAAiB2U,EAAWhf,EAAI+e,EAC1C,CACA,SAASqB,GAActhB,EAASyf,EAAQS,EAAW7B,EAASsB,GAC1D,MAAMze,EAAKse,GAAYC,EAAOS,GAAY7B,EAASsB,GAC9Cze,IAGLlB,EAAQyL,oBAAoByU,EAAWhf,EAAIqgB,QAAQ5B,WAC5CF,EAAOS,GAAWhf,EAAG8d,UAC9B,CACA,SAASwC,GAAyBxhB,EAASyf,EAAQS,EAAWuB,GAC5D,MAAMC,EAAoBjC,EAAOS,IAAc,CAAC,EAChD,IAAK,MAAOyB,EAAY9B,KAAUpiB,OAAOmkB,QAAQF,GAC3CC,EAAWE,SAASJ,IACtBH,GAActhB,EAASyf,EAAQS,EAAWL,EAAMH,SAAUG,EAAMF,mBAGtE,CACA,SAASQ,GAAaN,GAGpB,OADAA,EAAQA,EAAMjU,QAAQiT,GAAgB,IAC/BI,GAAaY,IAAUA,CAChC,CACA,MAAMmB,GAAe,CACnB,EAAAc,CAAG9hB,EAAS6f,EAAOxB,EAAS2B,GAC1BI,GAAWpgB,EAAS6f,EAAOxB,EAAS2B,GAAoB,EAC1D,EACA,GAAA+B,CAAI/hB,EAAS6f,EAAOxB,EAAS2B,GAC3BI,GAAWpgB,EAAS6f,EAAOxB,EAAS2B,GAAoB,EAC1D,EACA,GAAAiB,CAAIjhB,EAAS+f,EAAmB1B,EAAS2B,GACvC,GAAiC,iBAAtBD,IAAmC/f,EAC5C,OAEF,MAAOigB,EAAaP,EAAUQ,GAAaJ,GAAoBC,EAAmB1B,EAAS2B,GACrFgC,EAAc9B,IAAcH,EAC5BN,EAASF,GAAiBvf,GAC1B0hB,EAAoBjC,EAAOS,IAAc,CAAC,EAC1C+B,EAAclC,EAAkBmC,WAAW,KACjD,QAAwB,IAAbxC,EAAX,CAQA,GAAIuC,EACF,IAAK,MAAME,KAAgB1kB,OAAO4D,KAAKoe,GACrC+B,GAAyBxhB,EAASyf,EAAQ0C,EAAcpC,EAAkBlN,MAAM,IAGpF,IAAK,MAAOuP,EAAavC,KAAUpiB,OAAOmkB,QAAQF,GAAoB,CACpE,MAAMC,EAAaS,EAAYxW,QAAQkT,GAAe,IACjDkD,IAAejC,EAAkB8B,SAASF,IAC7CL,GAActhB,EAASyf,EAAQS,EAAWL,EAAMH,SAAUG,EAAMF,mBAEpE,CAXA,KAPA,CAEE,IAAKliB,OAAO4D,KAAKqgB,GAAmBvQ,OAClC,OAEFmQ,GAActhB,EAASyf,EAAQS,EAAWR,EAAUO,EAAc5B,EAAU,KAE9E,CAYF,EACA,OAAAgE,CAAQriB,EAAS6f,EAAOpI,GACtB,GAAqB,iBAAVoI,IAAuB7f,EAChC,OAAO,KAET,MAAM+c,EAAIR,KAGV,IAAI+F,EAAc,KACdC,GAAU,EACVC,GAAiB,EACjBC,GAAmB,EAJH5C,IADFM,GAAaN,IAMZ9C,IACjBuF,EAAcvF,EAAEhC,MAAM8E,EAAOpI,GAC7BsF,EAAE/c,GAASqiB,QAAQC,GACnBC,GAAWD,EAAYI,uBACvBF,GAAkBF,EAAYK,gCAC9BF,EAAmBH,EAAYM,sBAEjC,MAAMC,EAAM9B,GAAW,IAAIhG,MAAM8E,EAAO,CACtC0C,UACAO,YAAY,IACVrL,GAUJ,OATIgL,GACFI,EAAIE,iBAEFP,GACFxiB,EAAQ8a,cAAc+H,GAEpBA,EAAIJ,kBAAoBH,GAC1BA,EAAYS,iBAEPF,CACT,GAEF,SAAS9B,GAAWljB,EAAKmlB,EAAO,CAAC,GAC/B,IAAK,MAAOzlB,EAAKa,KAAUX,OAAOmkB,QAAQoB,GACxC,IACEnlB,EAAIN,GAAOa,CACb,CAAE,MAAO6kB,GACPxlB,OAAOC,eAAeG,EAAKN,EAAK,CAC9B2lB,cAAc,EACdtlB,IAAG,IACMQ,GAGb,CAEF,OAAOP,CACT,CASA,SAASslB,GAAc/kB,GACrB,GAAc,SAAVA,EACF,OAAO,EAET,GAAc,UAAVA,EACF,OAAO,EAET,GAAIA,IAAU4f,OAAO5f,GAAOkC,WAC1B,OAAO0d,OAAO5f,GAEhB,GAAc,KAAVA,GAA0B,SAAVA,EAClB,OAAO,KAET,GAAqB,iBAAVA,EACT,OAAOA,EAET,IACE,OAAOglB,KAAKC,MAAMC,mBAAmBllB,GACvC,CAAE,MAAO6kB,GACP,OAAO7kB,CACT,CACF,CACA,SAASmlB,GAAiBhmB,GACxB,OAAOA,EAAIqO,QAAQ,UAAU4X,GAAO,IAAIA,EAAItjB,iBAC9C,CACA,MAAMujB,GAAc,CAClB,gBAAAC,CAAiB1jB,EAASzC,EAAKa,GAC7B4B,EAAQ6B,aAAa,WAAW0hB,GAAiBhmB,KAAQa,EAC3D,EACA,mBAAAulB,CAAoB3jB,EAASzC,GAC3ByC,EAAQ4B,gBAAgB,WAAW2hB,GAAiBhmB,KACtD,EACA,iBAAAqmB,CAAkB5jB,GAChB,IAAKA,EACH,MAAO,CAAC,EAEV,MAAM0B,EAAa,CAAC,EACdmiB,EAASpmB,OAAO4D,KAAKrB,EAAQ8jB,SAASld,QAAOrJ,GAAOA,EAAI2kB,WAAW,QAAU3kB,EAAI2kB,WAAW,cAClG,IAAK,MAAM3kB,KAAOsmB,EAAQ,CACxB,IAAIE,EAAUxmB,EAAIqO,QAAQ,MAAO,IACjCmY,EAAUA,EAAQC,OAAO,GAAG9jB,cAAgB6jB,EAAQlR,MAAM,EAAGkR,EAAQ5S,QACrEzP,EAAWqiB,GAAWZ,GAAcnjB,EAAQ8jB,QAAQvmB,GACtD,CACA,OAAOmE,CACT,EACAuiB,iBAAgB,CAACjkB,EAASzC,IACjB4lB,GAAcnjB,EAAQic,aAAa,WAAWsH,GAAiBhmB,QAgB1E,MAAM2mB,GAEJ,kBAAWC,GACT,MAAO,CAAC,CACV,CACA,sBAAWC,GACT,MAAO,CAAC,CACV,CACA,eAAWpH,GACT,MAAM,IAAIqH,MAAM,sEAClB,CACA,UAAAC,CAAWC,GAIT,OAHAA,EAAS9D,KAAK+D,gBAAgBD,GAC9BA,EAAS9D,KAAKgE,kBAAkBF,GAChC9D,KAAKiE,iBAAiBH,GACfA,CACT,CACA,iBAAAE,CAAkBF,GAChB,OAAOA,CACT,CACA,eAAAC,CAAgBD,EAAQvkB,GACtB,MAAM2kB,EAAa,GAAU3kB,GAAWyjB,GAAYQ,iBAAiBjkB,EAAS,UAAY,CAAC,EAE3F,MAAO,IACFygB,KAAKmE,YAAYT,WACM,iBAAfQ,EAA0BA,EAAa,CAAC,KAC/C,GAAU3kB,GAAWyjB,GAAYG,kBAAkB5jB,GAAW,CAAC,KAC7C,iBAAXukB,EAAsBA,EAAS,CAAC,EAE/C,CACA,gBAAAG,CAAiBH,EAAQM,EAAcpE,KAAKmE,YAAYR,aACtD,IAAK,MAAO7hB,EAAUuiB,KAAkBrnB,OAAOmkB,QAAQiD,GAAc,CACnE,MAAMzmB,EAAQmmB,EAAOhiB,GACfwiB,EAAY,GAAU3mB,GAAS,UAjiBrC4c,OADSA,EAkiB+C5c,GAhiBnD,GAAG4c,IAELvd,OAAOM,UAAUuC,SAASrC,KAAK+c,GAAQL,MAAM,eAAe,GAAGza,cA+hBlE,IAAK,IAAI8kB,OAAOF,GAAehhB,KAAKihB,GAClC,MAAM,IAAIE,UAAU,GAAGxE,KAAKmE,YAAY5H,KAAKkI,0BAA0B3iB,qBAA4BwiB,yBAAiCD,MAExI,CAtiBW9J,KAuiBb,EAqBF,MAAMmK,WAAsBjB,GAC1B,WAAAU,CAAY5kB,EAASukB,GACnBa,SACAplB,EAAUmb,GAAWnb,MAIrBygB,KAAK4E,SAAWrlB,EAChBygB,KAAK6E,QAAU7E,KAAK6D,WAAWC,GAC/BzK,GAAKtH,IAAIiO,KAAK4E,SAAU5E,KAAKmE,YAAYW,SAAU9E,MACrD,CAGA,OAAA+E,GACE1L,GAAKM,OAAOqG,KAAK4E,SAAU5E,KAAKmE,YAAYW,UAC5CvE,GAAaC,IAAIR,KAAK4E,SAAU5E,KAAKmE,YAAYa,WACjD,IAAK,MAAMC,KAAgBjoB,OAAOkoB,oBAAoBlF,MACpDA,KAAKiF,GAAgB,IAEzB,CACA,cAAAE,CAAe9I,EAAU9c,EAAS6lB,GAAa,GAC7CpI,GAAuBX,EAAU9c,EAAS6lB,EAC5C,CACA,UAAAvB,CAAWC,GAIT,OAHAA,EAAS9D,KAAK+D,gBAAgBD,EAAQ9D,KAAK4E,UAC3Cd,EAAS9D,KAAKgE,kBAAkBF,GAChC9D,KAAKiE,iBAAiBH,GACfA,CACT,CAGA,kBAAOuB,CAAY9lB,GACjB,OAAO8Z,GAAKlc,IAAIud,GAAWnb,GAAUygB,KAAK8E,SAC5C,CACA,0BAAOQ,CAAoB/lB,EAASukB,EAAS,CAAC,GAC5C,OAAO9D,KAAKqF,YAAY9lB,IAAY,IAAIygB,KAAKzgB,EAA2B,iBAAXukB,EAAsBA,EAAS,KAC9F,CACA,kBAAWyB,GACT,MA5CY,OA6Cd,CACA,mBAAWT,GACT,MAAO,MAAM9E,KAAKzD,MACpB,CACA,oBAAWyI,GACT,MAAO,IAAIhF,KAAK8E,UAClB,CACA,gBAAOU,CAAUllB,GACf,MAAO,GAAGA,IAAO0f,KAAKgF,WACxB,EAUF,MAAMS,GAAclmB,IAClB,IAAIwa,EAAWxa,EAAQic,aAAa,kBACpC,IAAKzB,GAAyB,MAAbA,EAAkB,CACjC,IAAI2L,EAAgBnmB,EAAQic,aAAa,QAMzC,IAAKkK,IAAkBA,EAActE,SAAS,OAASsE,EAAcjE,WAAW,KAC9E,OAAO,KAILiE,EAActE,SAAS,OAASsE,EAAcjE,WAAW,OAC3DiE,EAAgB,IAAIA,EAAcxjB,MAAM,KAAK,MAE/C6X,EAAW2L,GAAmC,MAAlBA,EAAwB5L,GAAc4L,EAAcC,QAAU,IAC5F,CACA,OAAO5L,CAAQ,EAEX6L,GAAiB,CACrBzT,KAAI,CAAC4H,EAAUxa,EAAU8F,SAASC,kBACzB,GAAG3G,UAAUsB,QAAQ3C,UAAU8iB,iBAAiB5iB,KAAK+B,EAASwa,IAEvE8L,QAAO,CAAC9L,EAAUxa,EAAU8F,SAASC,kBAC5BrF,QAAQ3C,UAAU8K,cAAc5K,KAAK+B,EAASwa,GAEvD+L,SAAQ,CAACvmB,EAASwa,IACT,GAAGpb,UAAUY,EAAQumB,UAAU3f,QAAOzB,GAASA,EAAMqhB,QAAQhM,KAEtE,OAAAiM,CAAQzmB,EAASwa,GACf,MAAMiM,EAAU,GAChB,IAAIC,EAAW1mB,EAAQwF,WAAWiW,QAAQjB,GAC1C,KAAOkM,GACLD,EAAQpU,KAAKqU,GACbA,EAAWA,EAASlhB,WAAWiW,QAAQjB,GAEzC,OAAOiM,CACT,EACA,IAAAE,CAAK3mB,EAASwa,GACZ,IAAIoM,EAAW5mB,EAAQ6mB,uBACvB,KAAOD,GAAU,CACf,GAAIA,EAASJ,QAAQhM,GACnB,MAAO,CAACoM,GAEVA,EAAWA,EAASC,sBACtB,CACA,MAAO,EACT,EAEA,IAAAvhB,CAAKtF,EAASwa,GACZ,IAAIlV,EAAOtF,EAAQ8mB,mBACnB,KAAOxhB,GAAM,CACX,GAAIA,EAAKkhB,QAAQhM,GACf,MAAO,CAAClV,GAEVA,EAAOA,EAAKwhB,kBACd,CACA,MAAO,EACT,EACA,iBAAAC,CAAkB/mB,GAChB,MAAMgnB,EAAa,CAAC,IAAK,SAAU,QAAS,WAAY,SAAU,UAAW,aAAc,4BAA4BzjB,KAAIiX,GAAY,GAAGA,2BAAiC7W,KAAK,KAChL,OAAO8c,KAAK7N,KAAKoU,EAAYhnB,GAAS4G,QAAOqgB,IAAOtL,GAAWsL,IAAO7L,GAAU6L,IAClF,EACA,sBAAAC,CAAuBlnB,GACrB,MAAMwa,EAAW0L,GAAYlmB,GAC7B,OAAIwa,GACK6L,GAAeC,QAAQ9L,GAAYA,EAErC,IACT,EACA,sBAAA2M,CAAuBnnB,GACrB,MAAMwa,EAAW0L,GAAYlmB,GAC7B,OAAOwa,EAAW6L,GAAeC,QAAQ9L,GAAY,IACvD,EACA,+BAAA4M,CAAgCpnB,GAC9B,MAAMwa,EAAW0L,GAAYlmB,GAC7B,OAAOwa,EAAW6L,GAAezT,KAAK4H,GAAY,EACpD,GAUI6M,GAAuB,CAACC,EAAWC,EAAS,UAChD,MAAMC,EAAa,gBAAgBF,EAAU7B,YACvC1kB,EAAOumB,EAAUtK,KACvBgE,GAAac,GAAGhc,SAAU0hB,EAAY,qBAAqBzmB,OAAU,SAAU8e,GAI7E,GAHI,CAAC,IAAK,QAAQgC,SAASpB,KAAKgH,UAC9B5H,EAAMkD,iBAEJpH,GAAW8E,MACb,OAEF,MAAMzT,EAASqZ,GAAec,uBAAuB1G,OAASA,KAAKhF,QAAQ,IAAI1a,KAC9DumB,EAAUvB,oBAAoB/Y,GAGtCua,IACX,GAAE,EAiBEG,GAAc,YACdC,GAAc,QAAQD,KACtBE,GAAe,SAASF,KAQ9B,MAAMG,WAAc1C,GAElB,eAAWnI,GACT,MAfW,OAgBb,CAGA,KAAA8K,GAEE,GADmB9G,GAAaqB,QAAQ5B,KAAK4E,SAAUsC,IACxClF,iBACb,OAEFhC,KAAK4E,SAASvJ,UAAU1B,OAlBF,QAmBtB,MAAMyL,EAAapF,KAAK4E,SAASvJ,UAAU7W,SApBrB,QAqBtBwb,KAAKmF,gBAAe,IAAMnF,KAAKsH,mBAAmBtH,KAAK4E,SAAUQ,EACnE,CAGA,eAAAkC,GACEtH,KAAK4E,SAASjL,SACd4G,GAAaqB,QAAQ5B,KAAK4E,SAAUuC,IACpCnH,KAAK+E,SACP,CAGA,sBAAOtI,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAO+c,GAAM9B,oBAAoBtF,MACvC,GAAsB,iBAAX8D,EAAX,CAGA,QAAqB/K,IAAjB1O,EAAKyZ,IAAyBA,EAAOrC,WAAW,MAAmB,gBAAXqC,EAC1D,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,GAAQ9D,KAJb,CAKF,GACF,EAOF4G,GAAqBQ,GAAO,SAM5BjL,GAAmBiL,IAcnB,MAKMI,GAAyB,4BAO/B,MAAMC,WAAe/C,GAEnB,eAAWnI,GACT,MAfW,QAgBb,CAGA,MAAAmL,GAEE1H,KAAK4E,SAASxjB,aAAa,eAAgB4e,KAAK4E,SAASvJ,UAAUqM,OAjB3C,UAkB1B,CAGA,sBAAOjL,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAOod,GAAOnC,oBAAoBtF,MACzB,WAAX8D,GACFzZ,EAAKyZ,IAET,GACF,EAOFvD,GAAac,GAAGhc,SAjCe,2BAiCmBmiB,IAAwBpI,IACxEA,EAAMkD,iBACN,MAAMqF,EAASvI,EAAM7S,OAAOyO,QAAQwM,IACvBC,GAAOnC,oBAAoBqC,GACnCD,QAAQ,IAOfvL,GAAmBsL,IAcnB,MACMG,GAAc,YACdC,GAAmB,aAAaD,KAChCE,GAAkB,YAAYF,KAC9BG,GAAiB,WAAWH,KAC5BI,GAAoB,cAAcJ,KAClCK,GAAkB,YAAYL,KAK9BM,GAAY,CAChBC,YAAa,KACbC,aAAc,KACdC,cAAe,MAEXC,GAAgB,CACpBH,YAAa,kBACbC,aAAc,kBACdC,cAAe,mBAOjB,MAAME,WAAc9E,GAClB,WAAAU,CAAY5kB,EAASukB,GACnBa,QACA3E,KAAK4E,SAAWrlB,EACXA,GAAYgpB,GAAMC,gBAGvBxI,KAAK6E,QAAU7E,KAAK6D,WAAWC,GAC/B9D,KAAKyI,QAAU,EACfzI,KAAK0I,sBAAwB5H,QAAQlhB,OAAO+oB,cAC5C3I,KAAK4I,cACP,CAGA,kBAAWlF,GACT,OAAOwE,EACT,CACA,sBAAWvE,GACT,OAAO2E,EACT,CACA,eAAW/L,GACT,MA/CW,OAgDb,CAGA,OAAAwI,GACExE,GAAaC,IAAIR,KAAK4E,SAAUgD,GAClC,CAGA,MAAAiB,CAAOzJ,GACAY,KAAK0I,sBAIN1I,KAAK8I,wBAAwB1J,KAC/BY,KAAKyI,QAAUrJ,EAAM2J,SAJrB/I,KAAKyI,QAAUrJ,EAAM4J,QAAQ,GAAGD,OAMpC,CACA,IAAAE,CAAK7J,GACCY,KAAK8I,wBAAwB1J,KAC/BY,KAAKyI,QAAUrJ,EAAM2J,QAAU/I,KAAKyI,SAEtCzI,KAAKkJ,eACLrM,GAAQmD,KAAK6E,QAAQsD,YACvB,CACA,KAAAgB,CAAM/J,GACJY,KAAKyI,QAAUrJ,EAAM4J,SAAW5J,EAAM4J,QAAQtY,OAAS,EAAI,EAAI0O,EAAM4J,QAAQ,GAAGD,QAAU/I,KAAKyI,OACjG,CACA,YAAAS,GACE,MAAME,EAAYjnB,KAAKoC,IAAIyb,KAAKyI,SAChC,GAAIW,GAnEgB,GAoElB,OAEF,MAAM9b,EAAY8b,EAAYpJ,KAAKyI,QACnCzI,KAAKyI,QAAU,EACVnb,GAGLuP,GAAQvP,EAAY,EAAI0S,KAAK6E,QAAQwD,cAAgBrI,KAAK6E,QAAQuD,aACpE,CACA,WAAAQ,GACM5I,KAAK0I,uBACPnI,GAAac,GAAGrB,KAAK4E,SAAUoD,IAAmB5I,GAASY,KAAK6I,OAAOzJ,KACvEmB,GAAac,GAAGrB,KAAK4E,SAAUqD,IAAiB7I,GAASY,KAAKiJ,KAAK7J,KACnEY,KAAK4E,SAASvJ,UAAU5E,IAlFG,mBAoF3B8J,GAAac,GAAGrB,KAAK4E,SAAUiD,IAAkBzI,GAASY,KAAK6I,OAAOzJ,KACtEmB,GAAac,GAAGrB,KAAK4E,SAAUkD,IAAiB1I,GAASY,KAAKmJ,MAAM/J,KACpEmB,GAAac,GAAGrB,KAAK4E,SAAUmD,IAAgB3I,GAASY,KAAKiJ,KAAK7J,KAEtE,CACA,uBAAA0J,CAAwB1J,GACtB,OAAOY,KAAK0I,wBA3FS,QA2FiBtJ,EAAMiK,aA5FrB,UA4FyDjK,EAAMiK,YACxF,CAGA,kBAAOb,GACL,MAAO,iBAAkBnjB,SAASC,iBAAmB7C,UAAU6mB,eAAiB,CAClF,EAeF,MAEMC,GAAc,eACdC,GAAiB,YAKjBC,GAAa,OACbC,GAAa,OACbC,GAAiB,OACjBC,GAAkB,QAClBC,GAAc,QAAQN,KACtBO,GAAa,OAAOP,KACpBQ,GAAkB,UAAUR,KAC5BS,GAAqB,aAAaT,KAClCU,GAAqB,aAAaV,KAClCW,GAAmB,YAAYX,KAC/BY,GAAwB,OAAOZ,KAAcC,KAC7CY,GAAyB,QAAQb,KAAcC,KAC/Ca,GAAsB,WACtBC,GAAsB,SAMtBC,GAAkB,UAClBC,GAAgB,iBAChBC,GAAuBF,GAAkBC,GAKzCE,GAAmB,CACvB,UAAoBd,GACpB,WAAqBD,IAEjBgB,GAAY,CAChBC,SAAU,IACVC,UAAU,EACVC,MAAO,QACPC,MAAM,EACNC,OAAO,EACPC,MAAM,GAEFC,GAAgB,CACpBN,SAAU,mBAEVC,SAAU,UACVC,MAAO,mBACPC,KAAM,mBACNC,MAAO,UACPC,KAAM,WAOR,MAAME,WAAiBzG,GACrB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAKoL,UAAY,KACjBpL,KAAKqL,eAAiB,KACtBrL,KAAKsL,YAAa,EAClBtL,KAAKuL,aAAe,KACpBvL,KAAKwL,aAAe,KACpBxL,KAAKyL,mBAAqB7F,GAAeC,QArCjB,uBAqC8C7F,KAAK4E,UAC3E5E,KAAK0L,qBACD1L,KAAK6E,QAAQkG,OAASV,IACxBrK,KAAK2L,OAET,CAGA,kBAAWjI,GACT,OAAOiH,EACT,CACA,sBAAWhH,GACT,OAAOuH,EACT,CACA,eAAW3O,GACT,MAnFW,UAoFb,CAGA,IAAA1X,GACEmb,KAAK4L,OAAOnC,GACd,CACA,eAAAoC,IAIOxmB,SAASymB,QAAUnR,GAAUqF,KAAK4E,WACrC5E,KAAKnb,MAET,CACA,IAAAqhB,GACElG,KAAK4L,OAAOlC,GACd,CACA,KAAAoB,GACM9K,KAAKsL,YACPlR,GAAqB4F,KAAK4E,UAE5B5E,KAAK+L,gBACP,CACA,KAAAJ,GACE3L,KAAK+L,iBACL/L,KAAKgM,kBACLhM,KAAKoL,UAAYa,aAAY,IAAMjM,KAAK6L,mBAAmB7L,KAAK6E,QAAQ+F,SAC1E,CACA,iBAAAsB,GACOlM,KAAK6E,QAAQkG,OAGd/K,KAAKsL,WACP/K,GAAae,IAAItB,KAAK4E,SAAUkF,IAAY,IAAM9J,KAAK2L,UAGzD3L,KAAK2L,QACP,CACA,EAAAQ,CAAG1T,GACD,MAAM2T,EAAQpM,KAAKqM,YACnB,GAAI5T,EAAQ2T,EAAM1b,OAAS,GAAK+H,EAAQ,EACtC,OAEF,GAAIuH,KAAKsL,WAEP,YADA/K,GAAae,IAAItB,KAAK4E,SAAUkF,IAAY,IAAM9J,KAAKmM,GAAG1T,KAG5D,MAAM6T,EAActM,KAAKuM,cAAcvM,KAAKwM,cAC5C,GAAIF,IAAgB7T,EAClB,OAEF,MAAMtC,EAAQsC,EAAQ6T,EAAc7C,GAAaC,GACjD1J,KAAK4L,OAAOzV,EAAOiW,EAAM3T,GAC3B,CACA,OAAAsM,GACM/E,KAAKwL,cACPxL,KAAKwL,aAAazG,UAEpBJ,MAAMI,SACR,CAGA,iBAAAf,CAAkBF,GAEhB,OADAA,EAAO2I,gBAAkB3I,EAAO8G,SACzB9G,CACT,CACA,kBAAA4H,GACM1L,KAAK6E,QAAQgG,UACftK,GAAac,GAAGrB,KAAK4E,SAAUmF,IAAiB3K,GAASY,KAAK0M,SAAStN,KAE9C,UAAvBY,KAAK6E,QAAQiG,QACfvK,GAAac,GAAGrB,KAAK4E,SAAUoF,IAAoB,IAAMhK,KAAK8K,UAC9DvK,GAAac,GAAGrB,KAAK4E,SAAUqF,IAAoB,IAAMjK,KAAKkM,uBAE5DlM,KAAK6E,QAAQmG,OAASzC,GAAMC,eAC9BxI,KAAK2M,yBAET,CACA,uBAAAA,GACE,IAAK,MAAMC,KAAOhH,GAAezT,KArIX,qBAqImC6N,KAAK4E,UAC5DrE,GAAac,GAAGuL,EAAK1C,IAAkB9K,GAASA,EAAMkD,mBAExD,MAmBMuK,EAAc,CAClBzE,aAAc,IAAMpI,KAAK4L,OAAO5L,KAAK8M,kBAAkBnD,KACvDtB,cAAe,IAAMrI,KAAK4L,OAAO5L,KAAK8M,kBAAkBlD,KACxDzB,YAtBkB,KACS,UAAvBnI,KAAK6E,QAAQiG,QAYjB9K,KAAK8K,QACD9K,KAAKuL,cACPwB,aAAa/M,KAAKuL,cAEpBvL,KAAKuL,aAAe1N,YAAW,IAAMmC,KAAKkM,qBAjLjB,IAiL+DlM,KAAK6E,QAAQ+F,UAAS,GAOhH5K,KAAKwL,aAAe,IAAIjD,GAAMvI,KAAK4E,SAAUiI,EAC/C,CACA,QAAAH,CAAStN,GACP,GAAI,kBAAkB/b,KAAK+b,EAAM7S,OAAOya,SACtC,OAEF,MAAM1Z,EAAYod,GAAiBtL,EAAMtiB,KACrCwQ,IACF8R,EAAMkD,iBACNtC,KAAK4L,OAAO5L,KAAK8M,kBAAkBxf,IAEvC,CACA,aAAAif,CAAchtB,GACZ,OAAOygB,KAAKqM,YAAYlnB,QAAQ5F,EAClC,CACA,0BAAAytB,CAA2BvU,GACzB,IAAKuH,KAAKyL,mBACR,OAEF,MAAMwB,EAAkBrH,GAAeC,QAAQ0E,GAAiBvK,KAAKyL,oBACrEwB,EAAgB5R,UAAU1B,OAAO2Q,IACjC2C,EAAgB9rB,gBAAgB,gBAChC,MAAM+rB,EAAqBtH,GAAeC,QAAQ,sBAAsBpN,MAAWuH,KAAKyL,oBACpFyB,IACFA,EAAmB7R,UAAU5E,IAAI6T,IACjC4C,EAAmB9rB,aAAa,eAAgB,QAEpD,CACA,eAAA4qB,GACE,MAAMzsB,EAAUygB,KAAKqL,gBAAkBrL,KAAKwM,aAC5C,IAAKjtB,EACH,OAEF,MAAM4tB,EAAkB5P,OAAO6P,SAAS7tB,EAAQic,aAAa,oBAAqB,IAClFwE,KAAK6E,QAAQ+F,SAAWuC,GAAmBnN,KAAK6E,QAAQ4H,eAC1D,CACA,MAAAb,CAAOzV,EAAO5W,EAAU,MACtB,GAAIygB,KAAKsL,WACP,OAEF,MAAMvN,EAAgBiC,KAAKwM,aACrBa,EAASlX,IAAUsT,GACnB6D,EAAc/tB,GAAWue,GAAqBkC,KAAKqM,YAAatO,EAAesP,EAAQrN,KAAK6E,QAAQoG,MAC1G,GAAIqC,IAAgBvP,EAClB,OAEF,MAAMwP,EAAmBvN,KAAKuM,cAAce,GACtCE,EAAehI,GACZjF,GAAaqB,QAAQ5B,KAAK4E,SAAUY,EAAW,CACpD1F,cAAewN,EACfhgB,UAAW0S,KAAKyN,kBAAkBtX,GAClCuD,KAAMsG,KAAKuM,cAAcxO,GACzBoO,GAAIoB,IAIR,GADmBC,EAAa3D,IACjB7H,iBACb,OAEF,IAAKjE,IAAkBuP,EAGrB,OAEF,MAAMI,EAAY5M,QAAQd,KAAKoL,WAC/BpL,KAAK8K,QACL9K,KAAKsL,YAAa,EAClBtL,KAAKgN,2BAA2BO,GAChCvN,KAAKqL,eAAiBiC,EACtB,MAAMK,EAAuBN,EA3OR,sBADF,oBA6ObO,EAAiBP,EA3OH,qBACA,qBA2OpBC,EAAYjS,UAAU5E,IAAImX,GAC1B/R,GAAOyR,GACPvP,EAAc1C,UAAU5E,IAAIkX,GAC5BL,EAAYjS,UAAU5E,IAAIkX,GAQ1B3N,KAAKmF,gBAPoB,KACvBmI,EAAYjS,UAAU1B,OAAOgU,EAAsBC,GACnDN,EAAYjS,UAAU5E,IAAI6T,IAC1BvM,EAAc1C,UAAU1B,OAAO2Q,GAAqBsD,EAAgBD,GACpE3N,KAAKsL,YAAa,EAClBkC,EAAa1D,GAAW,GAEY/L,EAAeiC,KAAK6N,eACtDH,GACF1N,KAAK2L,OAET,CACA,WAAAkC,GACE,OAAO7N,KAAK4E,SAASvJ,UAAU7W,SAhQV,QAiQvB,CACA,UAAAgoB,GACE,OAAO5G,GAAeC,QAAQ4E,GAAsBzK,KAAK4E,SAC3D,CACA,SAAAyH,GACE,OAAOzG,GAAezT,KAAKqY,GAAexK,KAAK4E,SACjD,CACA,cAAAmH,GACM/L,KAAKoL,YACP0C,cAAc9N,KAAKoL,WACnBpL,KAAKoL,UAAY,KAErB,CACA,iBAAA0B,CAAkBxf,GAChB,OAAI2O,KACK3O,IAAcqc,GAAiBD,GAAaD,GAE9Cnc,IAAcqc,GAAiBF,GAAaC,EACrD,CACA,iBAAA+D,CAAkBtX,GAChB,OAAI8F,KACK9F,IAAUuT,GAAaC,GAAiBC,GAE1CzT,IAAUuT,GAAaE,GAAkBD,EAClD,CAGA,sBAAOlN,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAO8gB,GAAS7F,oBAAoBtF,KAAM8D,GAChD,GAAsB,iBAAXA,GAIX,GAAsB,iBAAXA,EAAqB,CAC9B,QAAqB/K,IAAjB1O,EAAKyZ,IAAyBA,EAAOrC,WAAW,MAAmB,gBAAXqC,EAC1D,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IACP,OAREzZ,EAAK8hB,GAAGrI,EASZ,GACF,EAOFvD,GAAac,GAAGhc,SAAU+kB,GAvSE,uCAuS2C,SAAUhL,GAC/E,MAAM7S,EAASqZ,GAAec,uBAAuB1G,MACrD,IAAKzT,IAAWA,EAAO8O,UAAU7W,SAAS6lB,IACxC,OAEFjL,EAAMkD,iBACN,MAAMyL,EAAW5C,GAAS7F,oBAAoB/Y,GACxCyhB,EAAahO,KAAKxE,aAAa,oBACrC,OAAIwS,GACFD,EAAS5B,GAAG6B,QACZD,EAAS7B,qBAGyC,SAAhDlJ,GAAYQ,iBAAiBxD,KAAM,UACrC+N,EAASlpB,YACTkpB,EAAS7B,sBAGX6B,EAAS7H,YACT6H,EAAS7B,oBACX,IACA3L,GAAac,GAAGzhB,OAAQuqB,IAAuB,KAC7C,MAAM8D,EAAYrI,GAAezT,KA5TR,6BA6TzB,IAAK,MAAM4b,KAAYE,EACrB9C,GAAS7F,oBAAoByI,EAC/B,IAOF5R,GAAmBgP,IAcnB,MAEM+C,GAAc,eAEdC,GAAe,OAAOD,KACtBE,GAAgB,QAAQF,KACxBG,GAAe,OAAOH,KACtBI,GAAiB,SAASJ,KAC1BK,GAAyB,QAAQL,cACjCM,GAAoB,OACpBC,GAAsB,WACtBC,GAAwB,aAExBC,GAA6B,WAAWF,OAAwBA,KAKhEG,GAAyB,8BACzBC,GAAY,CAChBpqB,OAAQ,KACRijB,QAAQ,GAEJoH,GAAgB,CACpBrqB,OAAQ,iBACRijB,OAAQ,WAOV,MAAMqH,WAAiBrK,GACrB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAKgP,kBAAmB,EACxBhP,KAAKiP,cAAgB,GACrB,MAAMC,EAAatJ,GAAezT,KAAKyc,IACvC,IAAK,MAAMO,KAAQD,EAAY,CAC7B,MAAMnV,EAAW6L,GAAea,uBAAuB0I,GACjDC,EAAgBxJ,GAAezT,KAAK4H,GAAU5T,QAAOkpB,GAAgBA,IAAiBrP,KAAK4E,WAChF,OAAb7K,GAAqBqV,EAAc1e,QACrCsP,KAAKiP,cAAcrd,KAAKud,EAE5B,CACAnP,KAAKsP,sBACAtP,KAAK6E,QAAQpgB,QAChBub,KAAKuP,0BAA0BvP,KAAKiP,cAAejP,KAAKwP,YAEtDxP,KAAK6E,QAAQ6C,QACf1H,KAAK0H,QAET,CAGA,kBAAWhE,GACT,OAAOmL,EACT,CACA,sBAAWlL,GACT,OAAOmL,EACT,CACA,eAAWvS,GACT,MA9DW,UA+Db,CAGA,MAAAmL,GACM1H,KAAKwP,WACPxP,KAAKyP,OAELzP,KAAK0P,MAET,CACA,IAAAA,GACE,GAAI1P,KAAKgP,kBAAoBhP,KAAKwP,WAChC,OAEF,IAAIG,EAAiB,GAQrB,GALI3P,KAAK6E,QAAQpgB,SACfkrB,EAAiB3P,KAAK4P,uBAhEH,wCAgE4CzpB,QAAO5G,GAAWA,IAAYygB,KAAK4E,WAAU9hB,KAAIvD,GAAWwvB,GAASzJ,oBAAoB/lB,EAAS,CAC/JmoB,QAAQ,OAGRiI,EAAejf,QAAUif,EAAe,GAAGX,iBAC7C,OAGF,GADmBzO,GAAaqB,QAAQ5B,KAAK4E,SAAUuJ,IACxCnM,iBACb,OAEF,IAAK,MAAM6N,KAAkBF,EAC3BE,EAAeJ,OAEjB,MAAMK,EAAY9P,KAAK+P,gBACvB/P,KAAK4E,SAASvJ,UAAU1B,OAAO8U,IAC/BzO,KAAK4E,SAASvJ,UAAU5E,IAAIiY,IAC5B1O,KAAK4E,SAAS7jB,MAAM+uB,GAAa,EACjC9P,KAAKuP,0BAA0BvP,KAAKiP,eAAe,GACnDjP,KAAKgP,kBAAmB,EACxB,MAQMgB,EAAa,SADUF,EAAU,GAAGrL,cAAgBqL,EAAU1d,MAAM,KAE1E4N,KAAKmF,gBATY,KACfnF,KAAKgP,kBAAmB,EACxBhP,KAAK4E,SAASvJ,UAAU1B,OAAO+U,IAC/B1O,KAAK4E,SAASvJ,UAAU5E,IAAIgY,GAAqBD,IACjDxO,KAAK4E,SAAS7jB,MAAM+uB,GAAa,GACjCvP,GAAaqB,QAAQ5B,KAAK4E,SAAUwJ,GAAc,GAItBpO,KAAK4E,UAAU,GAC7C5E,KAAK4E,SAAS7jB,MAAM+uB,GAAa,GAAG9P,KAAK4E,SAASoL,MACpD,CACA,IAAAP,GACE,GAAIzP,KAAKgP,mBAAqBhP,KAAKwP,WACjC,OAGF,GADmBjP,GAAaqB,QAAQ5B,KAAK4E,SAAUyJ,IACxCrM,iBACb,OAEF,MAAM8N,EAAY9P,KAAK+P,gBACvB/P,KAAK4E,SAAS7jB,MAAM+uB,GAAa,GAAG9P,KAAK4E,SAASthB,wBAAwBwsB,OAC1EjU,GAAOmE,KAAK4E,UACZ5E,KAAK4E,SAASvJ,UAAU5E,IAAIiY,IAC5B1O,KAAK4E,SAASvJ,UAAU1B,OAAO8U,GAAqBD,IACpD,IAAK,MAAM5M,KAAW5B,KAAKiP,cAAe,CACxC,MAAM1vB,EAAUqmB,GAAec,uBAAuB9E,GAClDriB,IAAYygB,KAAKwP,SAASjwB,IAC5BygB,KAAKuP,0BAA0B,CAAC3N,IAAU,EAE9C,CACA5B,KAAKgP,kBAAmB,EAOxBhP,KAAK4E,SAAS7jB,MAAM+uB,GAAa,GACjC9P,KAAKmF,gBAPY,KACfnF,KAAKgP,kBAAmB,EACxBhP,KAAK4E,SAASvJ,UAAU1B,OAAO+U,IAC/B1O,KAAK4E,SAASvJ,UAAU5E,IAAIgY,IAC5BlO,GAAaqB,QAAQ5B,KAAK4E,SAAU0J,GAAe,GAGvBtO,KAAK4E,UAAU,EAC/C,CACA,QAAA4K,CAASjwB,EAAUygB,KAAK4E,UACtB,OAAOrlB,EAAQ8b,UAAU7W,SAASgqB,GACpC,CAGA,iBAAAxK,CAAkBF,GAGhB,OAFAA,EAAO4D,OAAS5G,QAAQgD,EAAO4D,QAC/B5D,EAAOrf,OAASiW,GAAWoJ,EAAOrf,QAC3Bqf,CACT,CACA,aAAAiM,GACE,OAAO/P,KAAK4E,SAASvJ,UAAU7W,SA3IL,uBAChB,QACC,QA0Ib,CACA,mBAAA8qB,GACE,IAAKtP,KAAK6E,QAAQpgB,OAChB,OAEF,MAAMqhB,EAAW9F,KAAK4P,uBAAuBhB,IAC7C,IAAK,MAAMrvB,KAAWumB,EAAU,CAC9B,MAAMmK,EAAWrK,GAAec,uBAAuBnnB,GACnD0wB,GACFjQ,KAAKuP,0BAA0B,CAAChwB,GAAUygB,KAAKwP,SAASS,GAE5D,CACF,CACA,sBAAAL,CAAuB7V,GACrB,MAAM+L,EAAWF,GAAezT,KAAKwc,GAA4B3O,KAAK6E,QAAQpgB,QAE9E,OAAOmhB,GAAezT,KAAK4H,EAAUiG,KAAK6E,QAAQpgB,QAAQ0B,QAAO5G,IAAYumB,EAAS1E,SAAS7hB,IACjG,CACA,yBAAAgwB,CAA0BW,EAAcC,GACtC,GAAKD,EAAaxf,OAGlB,IAAK,MAAMnR,KAAW2wB,EACpB3wB,EAAQ8b,UAAUqM,OArKK,aAqKyByI,GAChD5wB,EAAQ6B,aAAa,gBAAiB+uB,EAE1C,CAGA,sBAAO1T,CAAgBqH,GACrB,MAAMe,EAAU,CAAC,EAIjB,MAHsB,iBAAXf,GAAuB,YAAYzgB,KAAKygB,KACjDe,EAAQ6C,QAAS,GAEZ1H,KAAKuH,MAAK,WACf,MAAMld,EAAO0kB,GAASzJ,oBAAoBtF,KAAM6E,GAChD,GAAsB,iBAAXf,EAAqB,CAC9B,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IACP,CACF,GACF,EAOFvD,GAAac,GAAGhc,SAAUkpB,GAAwBK,IAAwB,SAAUxP,IAErD,MAAzBA,EAAM7S,OAAOya,SAAmB5H,EAAMW,gBAAmD,MAAjCX,EAAMW,eAAeiH,UAC/E5H,EAAMkD,iBAER,IAAK,MAAM/iB,KAAWqmB,GAAee,gCAAgC3G,MACnE+O,GAASzJ,oBAAoB/lB,EAAS,CACpCmoB,QAAQ,IACPA,QAEP,IAMAvL,GAAmB4S,IAcnB,MAAMqB,GAAS,WAETC,GAAc,eACdC,GAAiB,YAGjBC,GAAiB,UACjBC,GAAmB,YAGnBC,GAAe,OAAOJ,KACtBK,GAAiB,SAASL,KAC1BM,GAAe,OAAON,KACtBO,GAAgB,QAAQP,KACxBQ,GAAyB,QAAQR,KAAcC,KAC/CQ,GAAyB,UAAUT,KAAcC,KACjDS,GAAuB,QAAQV,KAAcC,KAC7CU,GAAoB,OAMpBC,GAAyB,4DACzBC,GAA6B,GAAGD,MAA0BD,KAC1DG,GAAgB,iBAIhBC,GAAgBnV,KAAU,UAAY,YACtCoV,GAAmBpV,KAAU,YAAc,UAC3CqV,GAAmBrV,KAAU,aAAe,eAC5CsV,GAAsBtV,KAAU,eAAiB,aACjDuV,GAAkBvV,KAAU,aAAe,cAC3CwV,GAAiBxV,KAAU,cAAgB,aAG3CyV,GAAY,CAChBC,WAAW,EACX1jB,SAAU,kBACV2jB,QAAS,UACT5pB,OAAQ,CAAC,EAAG,GACZ6pB,aAAc,KACdvzB,UAAW,UAEPwzB,GAAgB,CACpBH,UAAW,mBACX1jB,SAAU,mBACV2jB,QAAS,SACT5pB,OAAQ,0BACR6pB,aAAc,yBACdvzB,UAAW,2BAOb,MAAMyzB,WAAiBrN,GACrB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAKgS,QAAU,KACfhS,KAAKiS,QAAUjS,KAAK4E,SAAS7f,WAE7Bib,KAAKkS,MAAQtM,GAAe/gB,KAAKmb,KAAK4E,SAAUuM,IAAe,IAAMvL,GAAeM,KAAKlG,KAAK4E,SAAUuM,IAAe,IAAMvL,GAAeC,QAAQsL,GAAenR,KAAKiS,SACxKjS,KAAKmS,UAAYnS,KAAKoS,eACxB,CAGA,kBAAW1O,GACT,OAAOgO,EACT,CACA,sBAAW/N,GACT,OAAOmO,EACT,CACA,eAAWvV,GACT,OAAO6T,EACT,CAGA,MAAA1I,GACE,OAAO1H,KAAKwP,WAAaxP,KAAKyP,OAASzP,KAAK0P,MAC9C,CACA,IAAAA,GACE,GAAIxU,GAAW8E,KAAK4E,WAAa5E,KAAKwP,WACpC,OAEF,MAAM1P,EAAgB,CACpBA,cAAeE,KAAK4E,UAGtB,IADkBrE,GAAaqB,QAAQ5B,KAAK4E,SAAU+L,GAAc7Q,GACtDkC,iBAAd,CASA,GANAhC,KAAKqS,gBAMD,iBAAkBhtB,SAASC,kBAAoB0a,KAAKiS,QAAQjX,QAzExC,eA0EtB,IAAK,MAAMzb,IAAW,GAAGZ,UAAU0G,SAAS6G,KAAK4Z,UAC/CvF,GAAac,GAAG9hB,EAAS,YAAaqc,IAG1CoE,KAAK4E,SAAS0N,QACdtS,KAAK4E,SAASxjB,aAAa,iBAAiB,GAC5C4e,KAAKkS,MAAM7W,UAAU5E,IAAIua,IACzBhR,KAAK4E,SAASvJ,UAAU5E,IAAIua,IAC5BzQ,GAAaqB,QAAQ5B,KAAK4E,SAAUgM,GAAe9Q,EAhBnD,CAiBF,CACA,IAAA2P,GACE,GAAIvU,GAAW8E,KAAK4E,YAAc5E,KAAKwP,WACrC,OAEF,MAAM1P,EAAgB,CACpBA,cAAeE,KAAK4E,UAEtB5E,KAAKuS,cAAczS,EACrB,CACA,OAAAiF,GACM/E,KAAKgS,SACPhS,KAAKgS,QAAQhZ,UAEf2L,MAAMI,SACR,CACA,MAAAha,GACEiV,KAAKmS,UAAYnS,KAAKoS,gBAClBpS,KAAKgS,SACPhS,KAAKgS,QAAQjnB,QAEjB,CAGA,aAAAwnB,CAAczS,GAEZ,IADkBS,GAAaqB,QAAQ5B,KAAK4E,SAAU6L,GAAc3Q,GACtDkC,iBAAd,CAMA,GAAI,iBAAkB3c,SAASC,gBAC7B,IAAK,MAAM/F,IAAW,GAAGZ,UAAU0G,SAAS6G,KAAK4Z,UAC/CvF,GAAaC,IAAIjhB,EAAS,YAAaqc,IAGvCoE,KAAKgS,SACPhS,KAAKgS,QAAQhZ,UAEfgH,KAAKkS,MAAM7W,UAAU1B,OAAOqX,IAC5BhR,KAAK4E,SAASvJ,UAAU1B,OAAOqX,IAC/BhR,KAAK4E,SAASxjB,aAAa,gBAAiB,SAC5C4hB,GAAYE,oBAAoBlD,KAAKkS,MAAO,UAC5C3R,GAAaqB,QAAQ5B,KAAK4E,SAAU8L,GAAgB5Q,EAhBpD,CAiBF,CACA,UAAA+D,CAAWC,GAET,GAAgC,iBADhCA,EAASa,MAAMd,WAAWC,IACRxlB,YAA2B,GAAUwlB,EAAOxlB,YAAgE,mBAA3CwlB,EAAOxlB,UAAUgF,sBAElG,MAAM,IAAIkhB,UAAU,GAAG4L,GAAO3L,+GAEhC,OAAOX,CACT,CACA,aAAAuO,GACE,QAAsB,IAAX,EACT,MAAM,IAAI7N,UAAU,gEAEtB,IAAIgO,EAAmBxS,KAAK4E,SACG,WAA3B5E,KAAK6E,QAAQvmB,UACfk0B,EAAmBxS,KAAKiS,QACf,GAAUjS,KAAK6E,QAAQvmB,WAChCk0B,EAAmB9X,GAAWsF,KAAK6E,QAAQvmB,WACA,iBAA3B0hB,KAAK6E,QAAQvmB,YAC7Bk0B,EAAmBxS,KAAK6E,QAAQvmB,WAElC,MAAMuzB,EAAe7R,KAAKyS,mBAC1BzS,KAAKgS,QAAU,GAAoBQ,EAAkBxS,KAAKkS,MAAOL,EACnE,CACA,QAAArC,GACE,OAAOxP,KAAKkS,MAAM7W,UAAU7W,SAASwsB,GACvC,CACA,aAAA0B,GACE,MAAMC,EAAiB3S,KAAKiS,QAC5B,GAAIU,EAAetX,UAAU7W,SArKN,WAsKrB,OAAOgtB,GAET,GAAImB,EAAetX,UAAU7W,SAvKJ,aAwKvB,OAAOitB,GAET,GAAIkB,EAAetX,UAAU7W,SAzKA,iBA0K3B,MA5JsB,MA8JxB,GAAImuB,EAAetX,UAAU7W,SA3KE,mBA4K7B,MA9JyB,SAkK3B,MAAMouB,EAAkF,QAA1E3tB,iBAAiB+a,KAAKkS,OAAOpX,iBAAiB,iBAAiB6K,OAC7E,OAAIgN,EAAetX,UAAU7W,SArLP,UAsLbouB,EAAQvB,GAAmBD,GAE7BwB,EAAQrB,GAAsBD,EACvC,CACA,aAAAc,GACE,OAAkD,OAA3CpS,KAAK4E,SAAS5J,QAnLD,UAoLtB,CACA,UAAA6X,GACE,MAAM,OACJ7qB,GACEgY,KAAK6E,QACT,MAAsB,iBAAX7c,EACFA,EAAO9F,MAAM,KAAKY,KAAInF,GAAS4f,OAAO6P,SAASzvB,EAAO,MAEzC,mBAAXqK,EACF8qB,GAAc9qB,EAAO8qB,EAAY9S,KAAK4E,UAExC5c,CACT,CACA,gBAAAyqB,GACE,MAAMM,EAAwB,CAC5Br0B,UAAWshB,KAAK0S,gBAChBtc,UAAW,CAAC,CACV9V,KAAM,kBACNmB,QAAS,CACPwM,SAAU+R,KAAK6E,QAAQ5W,WAExB,CACD3N,KAAM,SACNmB,QAAS,CACPuG,OAAQgY,KAAK6S,iBAanB,OAPI7S,KAAKmS,WAAsC,WAAzBnS,KAAK6E,QAAQ+M,WACjC5O,GAAYC,iBAAiBjD,KAAKkS,MAAO,SAAU,UACnDa,EAAsB3c,UAAY,CAAC,CACjC9V,KAAM,cACNC,SAAS,KAGN,IACFwyB,KACAlW,GAAQmD,KAAK6E,QAAQgN,aAAc,CAACkB,IAE3C,CACA,eAAAC,EAAgB,IACdl2B,EAAG,OACHyP,IAEA,MAAM6f,EAAQxG,GAAezT,KAhOF,8DAgO+B6N,KAAKkS,OAAO/rB,QAAO5G,GAAWob,GAAUpb,KAC7F6sB,EAAM1b,QAMXoN,GAAqBsO,EAAO7f,EAAQzP,IAAQ0zB,IAAmBpE,EAAMhL,SAAS7U,IAAS+lB,OACzF,CAGA,sBAAO7V,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAO0nB,GAASzM,oBAAoBtF,KAAM8D,GAChD,GAAsB,iBAAXA,EAAX,CAGA,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IAJL,CAKF,GACF,CACA,iBAAOmP,CAAW7T,GAChB,GA5QuB,IA4QnBA,EAAMuI,QAAgD,UAAfvI,EAAMqB,MA/QnC,QA+QuDrB,EAAMtiB,IACzE,OAEF,MAAMo2B,EAActN,GAAezT,KAAK+e,IACxC,IAAK,MAAMxJ,KAAUwL,EAAa,CAChC,MAAMC,EAAUpB,GAAS1M,YAAYqC,GACrC,IAAKyL,IAAyC,IAA9BA,EAAQtO,QAAQ8M,UAC9B,SAEF,MAAMyB,EAAehU,EAAMgU,eACrBC,EAAeD,EAAahS,SAAS+R,EAAQjB,OACnD,GAAIkB,EAAahS,SAAS+R,EAAQvO,WAA2C,WAA9BuO,EAAQtO,QAAQ8M,YAA2B0B,GAA8C,YAA9BF,EAAQtO,QAAQ8M,WAA2B0B,EACnJ,SAIF,GAAIF,EAAQjB,MAAM1tB,SAAS4a,EAAM7S,UAA2B,UAAf6S,EAAMqB,MA/RvC,QA+R2DrB,EAAMtiB,KAAqB,qCAAqCuG,KAAK+b,EAAM7S,OAAOya,UACvJ,SAEF,MAAMlH,EAAgB,CACpBA,cAAeqT,EAAQvO,UAEN,UAAfxF,EAAMqB,OACRX,EAAciH,WAAa3H,GAE7B+T,EAAQZ,cAAczS,EACxB,CACF,CACA,4BAAOwT,CAAsBlU,GAI3B,MAAMmU,EAAU,kBAAkBlwB,KAAK+b,EAAM7S,OAAOya,SAC9CwM,EAjTW,WAiTKpU,EAAMtiB,IACtB22B,EAAkB,CAAClD,GAAgBC,IAAkBpP,SAAShC,EAAMtiB,KAC1E,IAAK22B,IAAoBD,EACvB,OAEF,GAAID,IAAYC,EACd,OAEFpU,EAAMkD,iBAGN,MAAMoR,EAAkB1T,KAAK+F,QAAQkL,IAA0BjR,KAAO4F,GAAeM,KAAKlG,KAAMiR,IAAwB,IAAMrL,GAAe/gB,KAAKmb,KAAMiR,IAAwB,IAAMrL,GAAeC,QAAQoL,GAAwB7R,EAAMW,eAAehb,YACpPwF,EAAWwnB,GAASzM,oBAAoBoO,GAC9C,GAAID,EAIF,OAHArU,EAAMuU,kBACNppB,EAASmlB,YACTnlB,EAASyoB,gBAAgB5T,GAGvB7U,EAASilB,aAEXpQ,EAAMuU,kBACNppB,EAASklB,OACTiE,EAAgBpB,QAEpB,EAOF/R,GAAac,GAAGhc,SAAUyrB,GAAwBG,GAAwBc,GAASuB,uBACnF/S,GAAac,GAAGhc,SAAUyrB,GAAwBK,GAAeY,GAASuB,uBAC1E/S,GAAac,GAAGhc,SAAUwrB,GAAwBkB,GAASkB,YAC3D1S,GAAac,GAAGhc,SAAU0rB,GAAsBgB,GAASkB,YACzD1S,GAAac,GAAGhc,SAAUwrB,GAAwBI,IAAwB,SAAU7R,GAClFA,EAAMkD,iBACNyP,GAASzM,oBAAoBtF,MAAM0H,QACrC,IAMAvL,GAAmB4V,IAcnB,MAAM6B,GAAS,WAETC,GAAoB,OACpBC,GAAkB,gBAAgBF,KAClCG,GAAY,CAChBC,UAAW,iBACXC,cAAe,KACf7O,YAAY,EACZzK,WAAW,EAEXuZ,YAAa,QAGTC,GAAgB,CACpBH,UAAW,SACXC,cAAe,kBACf7O,WAAY,UACZzK,UAAW,UACXuZ,YAAa,oBAOf,MAAME,WAAiB3Q,GACrB,WAAAU,CAAYL,GACVa,QACA3E,KAAK6E,QAAU7E,KAAK6D,WAAWC,GAC/B9D,KAAKqU,aAAc,EACnBrU,KAAK4E,SAAW,IAClB,CAGA,kBAAWlB,GACT,OAAOqQ,EACT,CACA,sBAAWpQ,GACT,OAAOwQ,EACT,CACA,eAAW5X,GACT,OAAOqX,EACT,CAGA,IAAAlE,CAAKrT,GACH,IAAK2D,KAAK6E,QAAQlK,UAEhB,YADAkC,GAAQR,GAGV2D,KAAKsU,UACL,MAAM/0B,EAAUygB,KAAKuU,cACjBvU,KAAK6E,QAAQO,YACfvJ,GAAOtc,GAETA,EAAQ8b,UAAU5E,IAAIod,IACtB7T,KAAKwU,mBAAkB,KACrB3X,GAAQR,EAAS,GAErB,CACA,IAAAoT,CAAKpT,GACE2D,KAAK6E,QAAQlK,WAIlBqF,KAAKuU,cAAclZ,UAAU1B,OAAOka,IACpC7T,KAAKwU,mBAAkB,KACrBxU,KAAK+E,UACLlI,GAAQR,EAAS,KANjBQ,GAAQR,EAQZ,CACA,OAAA0I,GACO/E,KAAKqU,cAGV9T,GAAaC,IAAIR,KAAK4E,SAAUkP,IAChC9T,KAAK4E,SAASjL,SACdqG,KAAKqU,aAAc,EACrB,CAGA,WAAAE,GACE,IAAKvU,KAAK4E,SAAU,CAClB,MAAM6P,EAAWpvB,SAASqvB,cAAc,OACxCD,EAAST,UAAYhU,KAAK6E,QAAQmP,UAC9BhU,KAAK6E,QAAQO,YACfqP,EAASpZ,UAAU5E,IArFD,QAuFpBuJ,KAAK4E,SAAW6P,CAClB,CACA,OAAOzU,KAAK4E,QACd,CACA,iBAAAZ,CAAkBF,GAGhB,OADAA,EAAOoQ,YAAcxZ,GAAWoJ,EAAOoQ,aAChCpQ,CACT,CACA,OAAAwQ,GACE,GAAItU,KAAKqU,YACP,OAEF,MAAM90B,EAAUygB,KAAKuU,cACrBvU,KAAK6E,QAAQqP,YAAYS,OAAOp1B,GAChCghB,GAAac,GAAG9hB,EAASu0B,IAAiB,KACxCjX,GAAQmD,KAAK6E,QAAQoP,cAAc,IAErCjU,KAAKqU,aAAc,CACrB,CACA,iBAAAG,CAAkBnY,GAChBW,GAAuBX,EAAU2D,KAAKuU,cAAevU,KAAK6E,QAAQO,WACpE,EAeF,MAEMwP,GAAc,gBACdC,GAAkB,UAAUD,KAC5BE,GAAoB,cAAcF,KAGlCG,GAAmB,WACnBC,GAAY,CAChBC,WAAW,EACXC,YAAa,MAGTC,GAAgB,CACpBF,UAAW,UACXC,YAAa,WAOf,MAAME,WAAkB3R,GACtB,WAAAU,CAAYL,GACVa,QACA3E,KAAK6E,QAAU7E,KAAK6D,WAAWC,GAC/B9D,KAAKqV,WAAY,EACjBrV,KAAKsV,qBAAuB,IAC9B,CAGA,kBAAW5R,GACT,OAAOsR,EACT,CACA,sBAAWrR,GACT,OAAOwR,EACT,CACA,eAAW5Y,GACT,MAtCW,WAuCb,CAGA,QAAAgZ,GACMvV,KAAKqV,YAGLrV,KAAK6E,QAAQoQ,WACfjV,KAAK6E,QAAQqQ,YAAY5C,QAE3B/R,GAAaC,IAAInb,SAAUuvB,IAC3BrU,GAAac,GAAGhc,SAAUwvB,IAAiBzV,GAASY,KAAKwV,eAAepW,KACxEmB,GAAac,GAAGhc,SAAUyvB,IAAmB1V,GAASY,KAAKyV,eAAerW,KAC1EY,KAAKqV,WAAY,EACnB,CACA,UAAAK,GACO1V,KAAKqV,YAGVrV,KAAKqV,WAAY,EACjB9U,GAAaC,IAAInb,SAAUuvB,IAC7B,CAGA,cAAAY,CAAepW,GACb,MAAM,YACJ8V,GACElV,KAAK6E,QACT,GAAIzF,EAAM7S,SAAWlH,UAAY+Z,EAAM7S,SAAW2oB,GAAeA,EAAY1wB,SAAS4a,EAAM7S,QAC1F,OAEF,MAAM1L,EAAW+kB,GAAeU,kBAAkB4O,GAC1B,IAApBr0B,EAAS6P,OACXwkB,EAAY5C,QACHtS,KAAKsV,uBAAyBP,GACvCl0B,EAASA,EAAS6P,OAAS,GAAG4hB,QAE9BzxB,EAAS,GAAGyxB,OAEhB,CACA,cAAAmD,CAAerW,GA1ED,QA2ERA,EAAMtiB,MAGVkjB,KAAKsV,qBAAuBlW,EAAMuW,SAAWZ,GA7EzB,UA8EtB,EAeF,MAAMa,GAAyB,oDACzBC,GAA0B,cAC1BC,GAAmB,gBACnBC,GAAkB,eAMxB,MAAMC,GACJ,WAAA7R,GACEnE,KAAK4E,SAAWvf,SAAS6G,IAC3B,CAGA,QAAA+pB,GAEE,MAAMC,EAAgB7wB,SAASC,gBAAgBuC,YAC/C,OAAO1F,KAAKoC,IAAI3E,OAAOu2B,WAAaD,EACtC,CACA,IAAAzG,GACE,MAAM5rB,EAAQmc,KAAKiW,WACnBjW,KAAKoW,mBAELpW,KAAKqW,sBAAsBrW,KAAK4E,SAAUkR,IAAkBQ,GAAmBA,EAAkBzyB,IAEjGmc,KAAKqW,sBAAsBT,GAAwBE,IAAkBQ,GAAmBA,EAAkBzyB,IAC1Gmc,KAAKqW,sBAAsBR,GAAyBE,IAAiBO,GAAmBA,EAAkBzyB,GAC5G,CACA,KAAAwO,GACE2N,KAAKuW,wBAAwBvW,KAAK4E,SAAU,YAC5C5E,KAAKuW,wBAAwBvW,KAAK4E,SAAUkR,IAC5C9V,KAAKuW,wBAAwBX,GAAwBE,IACrD9V,KAAKuW,wBAAwBV,GAAyBE,GACxD,CACA,aAAAS,GACE,OAAOxW,KAAKiW,WAAa,CAC3B,CAGA,gBAAAG,GACEpW,KAAKyW,sBAAsBzW,KAAK4E,SAAU,YAC1C5E,KAAK4E,SAAS7jB,MAAM+K,SAAW,QACjC,CACA,qBAAAuqB,CAAsBtc,EAAU2c,EAAera,GAC7C,MAAMsa,EAAiB3W,KAAKiW,WAS5BjW,KAAK4W,2BAA2B7c,GARHxa,IAC3B,GAAIA,IAAYygB,KAAK4E,UAAYhlB,OAAOu2B,WAAa52B,EAAQsI,YAAc8uB,EACzE,OAEF3W,KAAKyW,sBAAsBl3B,EAASm3B,GACpC,MAAMJ,EAAkB12B,OAAOqF,iBAAiB1F,GAASub,iBAAiB4b,GAC1En3B,EAAQwB,MAAM81B,YAAYH,EAAe,GAAGra,EAASkB,OAAOC,WAAW8Y,QAAsB,GAGjG,CACA,qBAAAG,CAAsBl3B,EAASm3B,GAC7B,MAAMI,EAAcv3B,EAAQwB,MAAM+Z,iBAAiB4b,GAC/CI,GACF9T,GAAYC,iBAAiB1jB,EAASm3B,EAAeI,EAEzD,CACA,uBAAAP,CAAwBxc,EAAU2c,GAWhC1W,KAAK4W,2BAA2B7c,GAVHxa,IAC3B,MAAM5B,EAAQqlB,GAAYQ,iBAAiBjkB,EAASm3B,GAEtC,OAAV/4B,GAIJqlB,GAAYE,oBAAoB3jB,EAASm3B,GACzCn3B,EAAQwB,MAAM81B,YAAYH,EAAe/4B,IAJvC4B,EAAQwB,MAAMg2B,eAAeL,EAIgB,GAGnD,CACA,0BAAAE,CAA2B7c,EAAUid,GACnC,GAAI,GAAUjd,GACZid,EAASjd,QAGX,IAAK,MAAMkd,KAAOrR,GAAezT,KAAK4H,EAAUiG,KAAK4E,UACnDoS,EAASC,EAEb,EAeF,MAEMC,GAAc,YAGdC,GAAe,OAAOD,KACtBE,GAAyB,gBAAgBF,KACzCG,GAAiB,SAASH,KAC1BI,GAAe,OAAOJ,KACtBK,GAAgB,QAAQL,KACxBM,GAAiB,SAASN,KAC1BO,GAAsB,gBAAgBP,KACtCQ,GAA0B,oBAAoBR,KAC9CS,GAA0B,kBAAkBT,KAC5CU,GAAyB,QAAQV,cACjCW,GAAkB,aAElBC,GAAoB,OACpBC,GAAoB,eAKpBC,GAAY,CAChBvD,UAAU,EACVnC,OAAO,EACPzH,UAAU,GAENoN,GAAgB,CACpBxD,SAAU,mBACVnC,MAAO,UACPzH,SAAU,WAOZ,MAAMqN,WAAcxT,GAClB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAKmY,QAAUvS,GAAeC,QArBV,gBAqBmC7F,KAAK4E,UAC5D5E,KAAKoY,UAAYpY,KAAKqY,sBACtBrY,KAAKsY,WAAatY,KAAKuY,uBACvBvY,KAAKwP,UAAW,EAChBxP,KAAKgP,kBAAmB,EACxBhP,KAAKwY,WAAa,IAAIxC,GACtBhW,KAAK0L,oBACP,CAGA,kBAAWhI,GACT,OAAOsU,EACT,CACA,sBAAWrU,GACT,OAAOsU,EACT,CACA,eAAW1b,GACT,MA1DW,OA2Db,CAGA,MAAAmL,CAAO5H,GACL,OAAOE,KAAKwP,SAAWxP,KAAKyP,OAASzP,KAAK0P,KAAK5P,EACjD,CACA,IAAA4P,CAAK5P,GACCE,KAAKwP,UAAYxP,KAAKgP,kBAGRzO,GAAaqB,QAAQ5B,KAAK4E,SAAU0S,GAAc,CAClExX,kBAEYkC,mBAGdhC,KAAKwP,UAAW,EAChBxP,KAAKgP,kBAAmB,EACxBhP,KAAKwY,WAAW/I,OAChBpqB,SAAS6G,KAAKmP,UAAU5E,IAAIohB,IAC5B7X,KAAKyY,gBACLzY,KAAKoY,UAAU1I,MAAK,IAAM1P,KAAK0Y,aAAa5Y,KAC9C,CACA,IAAA2P,GACOzP,KAAKwP,WAAYxP,KAAKgP,mBAGTzO,GAAaqB,QAAQ5B,KAAK4E,SAAUuS,IACxCnV,mBAGdhC,KAAKwP,UAAW,EAChBxP,KAAKgP,kBAAmB,EACxBhP,KAAKsY,WAAW5C,aAChB1V,KAAK4E,SAASvJ,UAAU1B,OAAOme,IAC/B9X,KAAKmF,gBAAe,IAAMnF,KAAK2Y,cAAc3Y,KAAK4E,SAAU5E,KAAK6N,gBACnE,CACA,OAAA9I,GACExE,GAAaC,IAAI5gB,OAAQs3B,IACzB3W,GAAaC,IAAIR,KAAKmY,QAASjB,IAC/BlX,KAAKoY,UAAUrT,UACf/E,KAAKsY,WAAW5C,aAChB/Q,MAAMI,SACR,CACA,YAAA6T,GACE5Y,KAAKyY,eACP,CAGA,mBAAAJ,GACE,OAAO,IAAIjE,GAAS,CAClBzZ,UAAWmG,QAAQd,KAAK6E,QAAQ4P,UAEhCrP,WAAYpF,KAAK6N,eAErB,CACA,oBAAA0K,GACE,OAAO,IAAInD,GAAU,CACnBF,YAAalV,KAAK4E,UAEtB,CACA,YAAA8T,CAAa5Y,GAENza,SAAS6G,KAAK1H,SAASwb,KAAK4E,WAC/Bvf,SAAS6G,KAAKyoB,OAAO3U,KAAK4E,UAE5B5E,KAAK4E,SAAS7jB,MAAM6wB,QAAU,QAC9B5R,KAAK4E,SAASzjB,gBAAgB,eAC9B6e,KAAK4E,SAASxjB,aAAa,cAAc,GACzC4e,KAAK4E,SAASxjB,aAAa,OAAQ,UACnC4e,KAAK4E,SAASnZ,UAAY,EAC1B,MAAMotB,EAAYjT,GAAeC,QA7GT,cA6GsC7F,KAAKmY,SAC/DU,IACFA,EAAUptB,UAAY,GAExBoQ,GAAOmE,KAAK4E,UACZ5E,KAAK4E,SAASvJ,UAAU5E,IAAIqhB,IAU5B9X,KAAKmF,gBATsB,KACrBnF,KAAK6E,QAAQyN,OACftS,KAAKsY,WAAW/C,WAElBvV,KAAKgP,kBAAmB,EACxBzO,GAAaqB,QAAQ5B,KAAK4E,SAAU2S,GAAe,CACjDzX,iBACA,GAEoCE,KAAKmY,QAASnY,KAAK6N,cAC7D,CACA,kBAAAnC,GACEnL,GAAac,GAAGrB,KAAK4E,SAAU+S,IAAyBvY,IAhJvC,WAiJXA,EAAMtiB,MAGNkjB,KAAK6E,QAAQgG,SACf7K,KAAKyP,OAGPzP,KAAK8Y,6BAA4B,IAEnCvY,GAAac,GAAGzhB,OAAQ43B,IAAgB,KAClCxX,KAAKwP,WAAaxP,KAAKgP,kBACzBhP,KAAKyY,eACP,IAEFlY,GAAac,GAAGrB,KAAK4E,SAAU8S,IAAyBtY,IAEtDmB,GAAae,IAAItB,KAAK4E,SAAU6S,IAAqBsB,IAC/C/Y,KAAK4E,WAAaxF,EAAM7S,QAAUyT,KAAK4E,WAAamU,EAAOxsB,SAGjC,WAA1ByT,KAAK6E,QAAQ4P,SAIbzU,KAAK6E,QAAQ4P,UACfzU,KAAKyP,OAJLzP,KAAK8Y,6BAKP,GACA,GAEN,CACA,UAAAH,GACE3Y,KAAK4E,SAAS7jB,MAAM6wB,QAAU,OAC9B5R,KAAK4E,SAASxjB,aAAa,eAAe,GAC1C4e,KAAK4E,SAASzjB,gBAAgB,cAC9B6e,KAAK4E,SAASzjB,gBAAgB,QAC9B6e,KAAKgP,kBAAmB,EACxBhP,KAAKoY,UAAU3I,MAAK,KAClBpqB,SAAS6G,KAAKmP,UAAU1B,OAAOke,IAC/B7X,KAAKgZ,oBACLhZ,KAAKwY,WAAWnmB,QAChBkO,GAAaqB,QAAQ5B,KAAK4E,SAAUyS,GAAe,GAEvD,CACA,WAAAxJ,GACE,OAAO7N,KAAK4E,SAASvJ,UAAU7W,SAjLT,OAkLxB,CACA,0BAAAs0B,GAEE,GADkBvY,GAAaqB,QAAQ5B,KAAK4E,SAAUwS,IACxCpV,iBACZ,OAEF,MAAMiX,EAAqBjZ,KAAK4E,SAASvX,aAAehI,SAASC,gBAAgBsC,aAC3EsxB,EAAmBlZ,KAAK4E,SAAS7jB,MAAMiL,UAEpB,WAArBktB,GAAiClZ,KAAK4E,SAASvJ,UAAU7W,SAASuzB,MAGjEkB,IACHjZ,KAAK4E,SAAS7jB,MAAMiL,UAAY,UAElCgU,KAAK4E,SAASvJ,UAAU5E,IAAIshB,IAC5B/X,KAAKmF,gBAAe,KAClBnF,KAAK4E,SAASvJ,UAAU1B,OAAOoe,IAC/B/X,KAAKmF,gBAAe,KAClBnF,KAAK4E,SAAS7jB,MAAMiL,UAAYktB,CAAgB,GAC/ClZ,KAAKmY,QAAQ,GACfnY,KAAKmY,SACRnY,KAAK4E,SAAS0N,QAChB,CAMA,aAAAmG,GACE,MAAMQ,EAAqBjZ,KAAK4E,SAASvX,aAAehI,SAASC,gBAAgBsC,aAC3E+uB,EAAiB3W,KAAKwY,WAAWvC,WACjCkD,EAAoBxC,EAAiB,EAC3C,GAAIwC,IAAsBF,EAAoB,CAC5C,MAAMn3B,EAAWma,KAAU,cAAgB,eAC3C+D,KAAK4E,SAAS7jB,MAAMe,GAAY,GAAG60B,KACrC,CACA,IAAKwC,GAAqBF,EAAoB,CAC5C,MAAMn3B,EAAWma,KAAU,eAAiB,cAC5C+D,KAAK4E,SAAS7jB,MAAMe,GAAY,GAAG60B,KACrC,CACF,CACA,iBAAAqC,GACEhZ,KAAK4E,SAAS7jB,MAAMq4B,YAAc,GAClCpZ,KAAK4E,SAAS7jB,MAAMs4B,aAAe,EACrC,CAGA,sBAAO5c,CAAgBqH,EAAQhE,GAC7B,OAAOE,KAAKuH,MAAK,WACf,MAAMld,EAAO6tB,GAAM5S,oBAAoBtF,KAAM8D,GAC7C,GAAsB,iBAAXA,EAAX,CAGA,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,GAAQhE,EAJb,CAKF,GACF,EAOFS,GAAac,GAAGhc,SAAUuyB,GA9OK,4BA8O2C,SAAUxY,GAClF,MAAM7S,EAASqZ,GAAec,uBAAuB1G,MACjD,CAAC,IAAK,QAAQoB,SAASpB,KAAKgH,UAC9B5H,EAAMkD,iBAER/B,GAAae,IAAI/U,EAAQ+qB,IAAcgC,IACjCA,EAAUtX,kBAIdzB,GAAae,IAAI/U,EAAQ8qB,IAAgB,KACnC1c,GAAUqF,OACZA,KAAKsS,OACP,GACA,IAIJ,MAAMiH,EAAc3T,GAAeC,QAnQb,eAoQlB0T,GACFrB,GAAM7S,YAAYkU,GAAa9J,OAEpByI,GAAM5S,oBAAoB/Y,GAClCmb,OAAO1H,KACd,IACA4G,GAAqBsR,IAMrB/b,GAAmB+b,IAcnB,MAEMsB,GAAc,gBACdC,GAAiB,YACjBC,GAAwB,OAAOF,KAAcC,KAE7CE,GAAoB,OACpBC,GAAuB,UACvBC,GAAoB,SAEpBC,GAAgB,kBAChBC,GAAe,OAAOP,KACtBQ,GAAgB,QAAQR,KACxBS,GAAe,OAAOT,KACtBU,GAAuB,gBAAgBV,KACvCW,GAAiB,SAASX,KAC1BY,GAAe,SAASZ,KACxBa,GAAyB,QAAQb,KAAcC,KAC/Ca,GAAwB,kBAAkBd,KAE1Ce,GAAY,CAChB9F,UAAU,EACV5J,UAAU,EACVpgB,QAAQ,GAEJ+vB,GAAgB,CACpB/F,SAAU,mBACV5J,SAAU,UACVpgB,OAAQ,WAOV,MAAMgwB,WAAkB/V,GACtB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAKwP,UAAW,EAChBxP,KAAKoY,UAAYpY,KAAKqY,sBACtBrY,KAAKsY,WAAatY,KAAKuY,uBACvBvY,KAAK0L,oBACP,CAGA,kBAAWhI,GACT,OAAO6W,EACT,CACA,sBAAW5W,GACT,OAAO6W,EACT,CACA,eAAWje,GACT,MApDW,WAqDb,CAGA,MAAAmL,CAAO5H,GACL,OAAOE,KAAKwP,SAAWxP,KAAKyP,OAASzP,KAAK0P,KAAK5P,EACjD,CACA,IAAA4P,CAAK5P,GACCE,KAAKwP,UAGSjP,GAAaqB,QAAQ5B,KAAK4E,SAAUmV,GAAc,CAClEja,kBAEYkC,mBAGdhC,KAAKwP,UAAW,EAChBxP,KAAKoY,UAAU1I,OACV1P,KAAK6E,QAAQpa,SAChB,IAAIurB,IAAkBvG,OAExBzP,KAAK4E,SAASxjB,aAAa,cAAc,GACzC4e,KAAK4E,SAASxjB,aAAa,OAAQ,UACnC4e,KAAK4E,SAASvJ,UAAU5E,IAAImjB,IAW5B5Z,KAAKmF,gBAVoB,KAClBnF,KAAK6E,QAAQpa,SAAUuV,KAAK6E,QAAQ4P,UACvCzU,KAAKsY,WAAW/C,WAElBvV,KAAK4E,SAASvJ,UAAU5E,IAAIkjB,IAC5B3Z,KAAK4E,SAASvJ,UAAU1B,OAAOigB,IAC/BrZ,GAAaqB,QAAQ5B,KAAK4E,SAAUoV,GAAe,CACjDla,iBACA,GAEkCE,KAAK4E,UAAU,GACvD,CACA,IAAA6K,GACOzP,KAAKwP,WAGQjP,GAAaqB,QAAQ5B,KAAK4E,SAAUqV,IACxCjY,mBAGdhC,KAAKsY,WAAW5C,aAChB1V,KAAK4E,SAAS8V,OACd1a,KAAKwP,UAAW,EAChBxP,KAAK4E,SAASvJ,UAAU5E,IAAIojB,IAC5B7Z,KAAKoY,UAAU3I,OAUfzP,KAAKmF,gBAToB,KACvBnF,KAAK4E,SAASvJ,UAAU1B,OAAOggB,GAAmBE,IAClD7Z,KAAK4E,SAASzjB,gBAAgB,cAC9B6e,KAAK4E,SAASzjB,gBAAgB,QACzB6e,KAAK6E,QAAQpa,SAChB,IAAIurB,IAAkB3jB,QAExBkO,GAAaqB,QAAQ5B,KAAK4E,SAAUuV,GAAe,GAEfna,KAAK4E,UAAU,IACvD,CACA,OAAAG,GACE/E,KAAKoY,UAAUrT,UACf/E,KAAKsY,WAAW5C,aAChB/Q,MAAMI,SACR,CAGA,mBAAAsT,GACE,MASM1d,EAAYmG,QAAQd,KAAK6E,QAAQ4P,UACvC,OAAO,IAAIL,GAAS,CAClBJ,UA3HsB,qBA4HtBrZ,YACAyK,YAAY,EACZ8O,YAAalU,KAAK4E,SAAS7f,WAC3BkvB,cAAetZ,EAfK,KACU,WAA1BqF,KAAK6E,QAAQ4P,SAIjBzU,KAAKyP,OAHHlP,GAAaqB,QAAQ5B,KAAK4E,SAAUsV,GAG3B,EAUgC,MAE/C,CACA,oBAAA3B,GACE,OAAO,IAAInD,GAAU,CACnBF,YAAalV,KAAK4E,UAEtB,CACA,kBAAA8G,GACEnL,GAAac,GAAGrB,KAAK4E,SAAU0V,IAAuBlb,IA5IvC,WA6ITA,EAAMtiB,MAGNkjB,KAAK6E,QAAQgG,SACf7K,KAAKyP,OAGPlP,GAAaqB,QAAQ5B,KAAK4E,SAAUsV,IAAqB,GAE7D,CAGA,sBAAOzd,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAOowB,GAAUnV,oBAAoBtF,KAAM8D,GACjD,GAAsB,iBAAXA,EAAX,CAGA,QAAqB/K,IAAjB1O,EAAKyZ,IAAyBA,EAAOrC,WAAW,MAAmB,gBAAXqC,EAC1D,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,GAAQ9D,KAJb,CAKF,GACF,EAOFO,GAAac,GAAGhc,SAAUg1B,GA7JK,gCA6J2C,SAAUjb,GAClF,MAAM7S,EAASqZ,GAAec,uBAAuB1G,MAIrD,GAHI,CAAC,IAAK,QAAQoB,SAASpB,KAAKgH,UAC9B5H,EAAMkD,iBAEJpH,GAAW8E,MACb,OAEFO,GAAae,IAAI/U,EAAQ4tB,IAAgB,KAEnCxf,GAAUqF,OACZA,KAAKsS,OACP,IAIF,MAAMiH,EAAc3T,GAAeC,QAAQiU,IACvCP,GAAeA,IAAgBhtB,GACjCkuB,GAAUpV,YAAYkU,GAAa9J,OAExBgL,GAAUnV,oBAAoB/Y,GACtCmb,OAAO1H,KACd,IACAO,GAAac,GAAGzhB,OAAQ85B,IAAuB,KAC7C,IAAK,MAAM3f,KAAY6L,GAAezT,KAAK2nB,IACzCW,GAAUnV,oBAAoBvL,GAAU2V,MAC1C,IAEFnP,GAAac,GAAGzhB,OAAQw6B,IAAc,KACpC,IAAK,MAAM76B,KAAWqmB,GAAezT,KAAK,gDACG,UAAvClN,iBAAiB1F,GAASiC,UAC5Bi5B,GAAUnV,oBAAoB/lB,GAASkwB,MAE3C,IAEF7I,GAAqB6T,IAMrBte,GAAmBse,IAUnB,MACME,GAAmB,CAEvB,IAAK,CAAC,QAAS,MAAO,KAAM,OAAQ,OAHP,kBAI7B9pB,EAAG,CAAC,SAAU,OAAQ,QAAS,OAC/B+pB,KAAM,GACN9pB,EAAG,GACH+pB,GAAI,GACJC,IAAK,GACLC,KAAM,GACNC,IAAK,GACLC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJC,GAAI,GACJnqB,EAAG,GACHub,IAAK,CAAC,MAAO,SAAU,MAAO,QAAS,QAAS,UAChD6O,GAAI,GACJC,GAAI,GACJC,EAAG,GACHC,IAAK,GACLC,EAAG,GACHC,MAAO,GACPC,KAAM,GACNC,IAAK,GACLC,IAAK,GACLC,OAAQ,GACRC,EAAG,GACHC,GAAI,IAIAC,GAAgB,IAAI/lB,IAAI,CAAC,aAAc,OAAQ,OAAQ,WAAY,WAAY,SAAU,MAAO,eAShGgmB,GAAmB,0DACnBC,GAAmB,CAACx6B,EAAWy6B,KACnC,MAAMC,EAAgB16B,EAAUvC,SAASC,cACzC,OAAI+8B,EAAqBpb,SAASqb,IAC5BJ,GAAc1lB,IAAI8lB,IACb3b,QAAQwb,GAAiBj5B,KAAKtB,EAAU26B,YAM5CF,EAAqBr2B,QAAOw2B,GAAkBA,aAA0BpY,SAAQ9R,MAAKmqB,GAASA,EAAMv5B,KAAKo5B,IAAe,EA0C3HI,GAAY,CAChBC,UAAWnC,GACXoC,QAAS,CAAC,EAEVC,WAAY,GACZnwB,MAAM,EACNowB,UAAU,EACVC,WAAY,KACZC,SAAU,eAENC,GAAgB,CACpBN,UAAW,SACXC,QAAS,SACTC,WAAY,oBACZnwB,KAAM,UACNowB,SAAU,UACVC,WAAY,kBACZC,SAAU,UAENE,GAAqB,CACzBC,MAAO,iCACPvjB,SAAU,oBAOZ,MAAMwjB,WAAwB9Z,GAC5B,WAAAU,CAAYL,GACVa,QACA3E,KAAK6E,QAAU7E,KAAK6D,WAAWC,EACjC,CAGA,kBAAWJ,GACT,OAAOmZ,EACT,CACA,sBAAWlZ,GACT,OAAOyZ,EACT,CACA,eAAW7gB,GACT,MA3CW,iBA4Cb,CAGA,UAAAihB,GACE,OAAOxgC,OAAOmiB,OAAOa,KAAK6E,QAAQkY,SAASj6B,KAAIghB,GAAU9D,KAAKyd,yBAAyB3Z,KAAS3d,OAAO2a,QACzG,CACA,UAAA4c,GACE,OAAO1d,KAAKwd,aAAa9sB,OAAS,CACpC,CACA,aAAAitB,CAAcZ,GAMZ,OALA/c,KAAK4d,cAAcb,GACnB/c,KAAK6E,QAAQkY,QAAU,IAClB/c,KAAK6E,QAAQkY,WACbA,GAEE/c,IACT,CACA,MAAA6d,GACE,MAAMC,EAAkBz4B,SAASqvB,cAAc,OAC/CoJ,EAAgBC,UAAY/d,KAAKge,eAAehe,KAAK6E,QAAQsY,UAC7D,IAAK,MAAOpjB,EAAUkkB,KAASjhC,OAAOmkB,QAAQnB,KAAK6E,QAAQkY,SACzD/c,KAAKke,YAAYJ,EAAiBG,EAAMlkB,GAE1C,MAAMojB,EAAWW,EAAgBhY,SAAS,GACpCkX,EAAahd,KAAKyd,yBAAyBzd,KAAK6E,QAAQmY,YAI9D,OAHIA,GACFG,EAAS9hB,UAAU5E,OAAOumB,EAAW96B,MAAM,MAEtCi7B,CACT,CAGA,gBAAAlZ,CAAiBH,GACfa,MAAMV,iBAAiBH,GACvB9D,KAAK4d,cAAc9Z,EAAOiZ,QAC5B,CACA,aAAAa,CAAcO,GACZ,IAAK,MAAOpkB,EAAUgjB,KAAY//B,OAAOmkB,QAAQgd,GAC/CxZ,MAAMV,iBAAiB,CACrBlK,WACAujB,MAAOP,GACNM,GAEP,CACA,WAAAa,CAAYf,EAAUJ,EAAShjB,GAC7B,MAAMqkB,EAAkBxY,GAAeC,QAAQ9L,EAAUojB,GACpDiB,KAGLrB,EAAU/c,KAAKyd,yBAAyBV,IAKpC,GAAUA,GACZ/c,KAAKqe,sBAAsB3jB,GAAWqiB,GAAUqB,GAG9Cpe,KAAK6E,QAAQhY,KACfuxB,EAAgBL,UAAY/d,KAAKge,eAAejB,GAGlDqB,EAAgBE,YAAcvB,EAX5BqB,EAAgBzkB,SAYpB,CACA,cAAAqkB,CAAeG,GACb,OAAOne,KAAK6E,QAAQoY,SApJxB,SAAsBsB,EAAYzB,EAAW0B,GAC3C,IAAKD,EAAW7tB,OACd,OAAO6tB,EAET,GAAIC,GAAgD,mBAArBA,EAC7B,OAAOA,EAAiBD,GAE1B,MACME,GADY,IAAI7+B,OAAO8+B,WACKC,gBAAgBJ,EAAY,aACxD19B,EAAW,GAAGlC,UAAU8/B,EAAgBvyB,KAAKkU,iBAAiB,MACpE,IAAK,MAAM7gB,KAAWsB,EAAU,CAC9B,MAAM+9B,EAAcr/B,EAAQC,SAASC,cACrC,IAAKzC,OAAO4D,KAAKk8B,GAAW1b,SAASwd,GAAc,CACjDr/B,EAAQoa,SACR,QACF,CACA,MAAMklB,EAAgB,GAAGlgC,UAAUY,EAAQ0B,YACrC69B,EAAoB,GAAGngC,OAAOm+B,EAAU,MAAQ,GAAIA,EAAU8B,IAAgB,IACpF,IAAK,MAAM78B,KAAa88B,EACjBtC,GAAiBx6B,EAAW+8B,IAC/Bv/B,EAAQ4B,gBAAgBY,EAAUvC,SAGxC,CACA,OAAOi/B,EAAgBvyB,KAAK6xB,SAC9B,CA2HmCgB,CAAaZ,EAAKne,KAAK6E,QAAQiY,UAAW9c,KAAK6E,QAAQqY,YAAciB,CACtG,CACA,wBAAAV,CAAyBU,GACvB,OAAOthB,GAAQshB,EAAK,CAACne,MACvB,CACA,qBAAAqe,CAAsB9+B,EAAS6+B,GAC7B,GAAIpe,KAAK6E,QAAQhY,KAGf,OAFAuxB,EAAgBL,UAAY,QAC5BK,EAAgBzJ,OAAOp1B,GAGzB6+B,EAAgBE,YAAc/+B,EAAQ++B,WACxC,EAeF,MACMU,GAAwB,IAAI1oB,IAAI,CAAC,WAAY,YAAa,eAC1D2oB,GAAoB,OAEpBC,GAAoB,OAEpBC,GAAiB,SACjBC,GAAmB,gBACnBC,GAAgB,QAChBC,GAAgB,QAahBC,GAAgB,CACpBC,KAAM,OACNC,IAAK,MACLC,MAAOzjB,KAAU,OAAS,QAC1B0jB,OAAQ,SACRC,KAAM3jB,KAAU,QAAU,QAEtB4jB,GAAY,CAChB/C,UAAWnC,GACXmF,WAAW,EACX7xB,SAAU,kBACV8xB,WAAW,EACXC,YAAa,GACbC,MAAO,EACPjwB,mBAAoB,CAAC,MAAO,QAAS,SAAU,QAC/CnD,MAAM,EACN7E,OAAQ,CAAC,EAAG,GACZtJ,UAAW,MACXmzB,aAAc,KACdoL,UAAU,EACVC,WAAY,KACZnjB,UAAU,EACVojB,SAAU,+GACV+C,MAAO,GACPte,QAAS,eAELue,GAAgB,CACpBrD,UAAW,SACXgD,UAAW,UACX7xB,SAAU,mBACV8xB,UAAW,2BACXC,YAAa,oBACbC,MAAO,kBACPjwB,mBAAoB,QACpBnD,KAAM,UACN7E,OAAQ,0BACRtJ,UAAW,oBACXmzB,aAAc,yBACdoL,SAAU,UACVC,WAAY,kBACZnjB,SAAU,mBACVojB,SAAU,SACV+C,MAAO,4BACPte,QAAS,UAOX,MAAMwe,WAAgB1b,GACpB,WAAAP,CAAY5kB,EAASukB,GACnB,QAAsB,IAAX,EACT,MAAM,IAAIU,UAAU,+DAEtBG,MAAMplB,EAASukB,GAGf9D,KAAKqgB,YAAa,EAClBrgB,KAAKsgB,SAAW,EAChBtgB,KAAKugB,WAAa,KAClBvgB,KAAKwgB,eAAiB,CAAC,EACvBxgB,KAAKgS,QAAU,KACfhS,KAAKygB,iBAAmB,KACxBzgB,KAAK0gB,YAAc,KAGnB1gB,KAAK2gB,IAAM,KACX3gB,KAAK4gB,gBACA5gB,KAAK6E,QAAQ9K,UAChBiG,KAAK6gB,WAET,CAGA,kBAAWnd,GACT,OAAOmc,EACT,CACA,sBAAWlc,GACT,OAAOwc,EACT,CACA,eAAW5jB,GACT,MAxGW,SAyGb,CAGA,MAAAukB,GACE9gB,KAAKqgB,YAAa,CACpB,CACA,OAAAU,GACE/gB,KAAKqgB,YAAa,CACpB,CACA,aAAAW,GACEhhB,KAAKqgB,YAAcrgB,KAAKqgB,UAC1B,CACA,MAAA3Y,GACO1H,KAAKqgB,aAGVrgB,KAAKwgB,eAAeS,OAASjhB,KAAKwgB,eAAeS,MAC7CjhB,KAAKwP,WACPxP,KAAKkhB,SAGPlhB,KAAKmhB,SACP,CACA,OAAApc,GACEgI,aAAa/M,KAAKsgB,UAClB/f,GAAaC,IAAIR,KAAK4E,SAAS5J,QAAQmkB,IAAiBC,GAAkBpf,KAAKohB,mBAC3EphB,KAAK4E,SAASpJ,aAAa,2BAC7BwE,KAAK4E,SAASxjB,aAAa,QAAS4e,KAAK4E,SAASpJ,aAAa,2BAEjEwE,KAAKqhB,iBACL1c,MAAMI,SACR,CACA,IAAA2K,GACE,GAAoC,SAAhC1P,KAAK4E,SAAS7jB,MAAM6wB,QACtB,MAAM,IAAIhO,MAAM,uCAElB,IAAM5D,KAAKshB,mBAAoBthB,KAAKqgB,WAClC,OAEF,MAAM/G,EAAY/Y,GAAaqB,QAAQ5B,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UAlItD,SAoIX+b,GADa9lB,GAAeuE,KAAK4E,WACL5E,KAAK4E,SAAS9kB,cAAcwF,iBAAiBd,SAASwb,KAAK4E,UAC7F,GAAI0U,EAAUtX,mBAAqBuf,EACjC,OAIFvhB,KAAKqhB,iBACL,MAAMV,EAAM3gB,KAAKwhB,iBACjBxhB,KAAK4E,SAASxjB,aAAa,mBAAoBu/B,EAAInlB,aAAa,OAChE,MAAM,UACJukB,GACE/f,KAAK6E,QAYT,GAXK7E,KAAK4E,SAAS9kB,cAAcwF,gBAAgBd,SAASwb,KAAK2gB,OAC7DZ,EAAUpL,OAAOgM,GACjBpgB,GAAaqB,QAAQ5B,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UAhJpC,cAkJnBxF,KAAKgS,QAAUhS,KAAKqS,cAAcsO,GAClCA,EAAItlB,UAAU5E,IAAIyoB,IAMd,iBAAkB75B,SAASC,gBAC7B,IAAK,MAAM/F,IAAW,GAAGZ,UAAU0G,SAAS6G,KAAK4Z,UAC/CvF,GAAac,GAAG9hB,EAAS,YAAaqc,IAU1CoE,KAAKmF,gBAPY,KACf5E,GAAaqB,QAAQ5B,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UAhKrC,WAiKQ,IAApBxF,KAAKugB,YACPvgB,KAAKkhB,SAEPlhB,KAAKugB,YAAa,CAAK,GAEKvgB,KAAK2gB,IAAK3gB,KAAK6N,cAC/C,CACA,IAAA4B,GACE,GAAKzP,KAAKwP,aAGQjP,GAAaqB,QAAQ5B,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UA/KtD,SAgLHxD,iBAAd,CAQA,GALYhC,KAAKwhB,iBACbnmB,UAAU1B,OAAOulB,IAIjB,iBAAkB75B,SAASC,gBAC7B,IAAK,MAAM/F,IAAW,GAAGZ,UAAU0G,SAAS6G,KAAK4Z,UAC/CvF,GAAaC,IAAIjhB,EAAS,YAAaqc,IAG3CoE,KAAKwgB,eAA4B,OAAI,EACrCxgB,KAAKwgB,eAAelB,KAAiB,EACrCtf,KAAKwgB,eAAenB,KAAiB,EACrCrf,KAAKugB,WAAa,KAYlBvgB,KAAKmF,gBAVY,KACXnF,KAAKyhB,yBAGJzhB,KAAKugB,YACRvgB,KAAKqhB,iBAEPrhB,KAAK4E,SAASzjB,gBAAgB,oBAC9Bof,GAAaqB,QAAQ5B,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UAzMpC,WAyM8D,GAEnDxF,KAAK2gB,IAAK3gB,KAAK6N,cA1B7C,CA2BF,CACA,MAAA9iB,GACMiV,KAAKgS,SACPhS,KAAKgS,QAAQjnB,QAEjB,CAGA,cAAAu2B,GACE,OAAOxgB,QAAQd,KAAK0hB,YACtB,CACA,cAAAF,GAIE,OAHKxhB,KAAK2gB,MACR3gB,KAAK2gB,IAAM3gB,KAAK2hB,kBAAkB3hB,KAAK0gB,aAAe1gB,KAAK4hB,2BAEtD5hB,KAAK2gB,GACd,CACA,iBAAAgB,CAAkB5E,GAChB,MAAM4D,EAAM3gB,KAAK6hB,oBAAoB9E,GAASc,SAG9C,IAAK8C,EACH,OAAO,KAETA,EAAItlB,UAAU1B,OAAOslB,GAAmBC,IAExCyB,EAAItlB,UAAU5E,IAAI,MAAMuJ,KAAKmE,YAAY5H,aACzC,MAAMulB,EAvuGKC,KACb,GACEA,GAAU5/B,KAAK6/B,MA/BH,IA+BS7/B,KAAK8/B,gBACnB58B,SAAS68B,eAAeH,IACjC,OAAOA,CAAM,EAmuGGI,CAAOniB,KAAKmE,YAAY5H,MAAM1c,WAK5C,OAJA8gC,EAAIv/B,aAAa,KAAM0gC,GACnB9hB,KAAK6N,eACP8S,EAAItlB,UAAU5E,IAAIwoB,IAEb0B,CACT,CACA,UAAAyB,CAAWrF,GACT/c,KAAK0gB,YAAc3D,EACf/c,KAAKwP,aACPxP,KAAKqhB,iBACLrhB,KAAK0P,OAET,CACA,mBAAAmS,CAAoB9E,GAYlB,OAXI/c,KAAKygB,iBACPzgB,KAAKygB,iBAAiB9C,cAAcZ,GAEpC/c,KAAKygB,iBAAmB,IAAIlD,GAAgB,IACvCvd,KAAK6E,QAGRkY,UACAC,WAAYhd,KAAKyd,yBAAyBzd,KAAK6E,QAAQmb,eAGpDhgB,KAAKygB,gBACd,CACA,sBAAAmB,GACE,MAAO,CACL,iBAA0B5hB,KAAK0hB,YAEnC,CACA,SAAAA,GACE,OAAO1hB,KAAKyd,yBAAyBzd,KAAK6E,QAAQqb,QAAUlgB,KAAK4E,SAASpJ,aAAa,yBACzF,CAGA,4BAAA6mB,CAA6BjjB,GAC3B,OAAOY,KAAKmE,YAAYmB,oBAAoBlG,EAAMW,eAAgBC,KAAKsiB,qBACzE,CACA,WAAAzU,GACE,OAAO7N,KAAK6E,QAAQib,WAAa9f,KAAK2gB,KAAO3gB,KAAK2gB,IAAItlB,UAAU7W,SAASy6B,GAC3E,CACA,QAAAzP,GACE,OAAOxP,KAAK2gB,KAAO3gB,KAAK2gB,IAAItlB,UAAU7W,SAAS06B,GACjD,CACA,aAAA7M,CAAcsO,GACZ,MAAMjiC,EAAYme,GAAQmD,KAAK6E,QAAQnmB,UAAW,CAACshB,KAAM2gB,EAAK3gB,KAAK4E,WAC7D2d,EAAahD,GAAc7gC,EAAU+lB,eAC3C,OAAO,GAAoBzE,KAAK4E,SAAU+b,EAAK3gB,KAAKyS,iBAAiB8P,GACvE,CACA,UAAA1P,GACE,MAAM,OACJ7qB,GACEgY,KAAK6E,QACT,MAAsB,iBAAX7c,EACFA,EAAO9F,MAAM,KAAKY,KAAInF,GAAS4f,OAAO6P,SAASzvB,EAAO,MAEzC,mBAAXqK,EACF8qB,GAAc9qB,EAAO8qB,EAAY9S,KAAK4E,UAExC5c,CACT,CACA,wBAAAy1B,CAAyBU,GACvB,OAAOthB,GAAQshB,EAAK,CAACne,KAAK4E,UAC5B,CACA,gBAAA6N,CAAiB8P,GACf,MAAMxP,EAAwB,CAC5Br0B,UAAW6jC,EACXnsB,UAAW,CAAC,CACV9V,KAAM,OACNmB,QAAS,CACPuO,mBAAoBgQ,KAAK6E,QAAQ7U,qBAElC,CACD1P,KAAM,SACNmB,QAAS,CACPuG,OAAQgY,KAAK6S,eAEd,CACDvyB,KAAM,kBACNmB,QAAS,CACPwM,SAAU+R,KAAK6E,QAAQ5W,WAExB,CACD3N,KAAM,QACNmB,QAAS,CACPlC,QAAS,IAAIygB,KAAKmE,YAAY5H,eAE/B,CACDjc,KAAM,kBACNC,SAAS,EACTC,MAAO,aACPC,GAAI4J,IAGF2V,KAAKwhB,iBAAiBpgC,aAAa,wBAAyBiJ,EAAK1J,MAAMjC,UAAU,KAIvF,MAAO,IACFq0B,KACAlW,GAAQmD,KAAK6E,QAAQgN,aAAc,CAACkB,IAE3C,CACA,aAAA6N,GACE,MAAM4B,EAAWxiB,KAAK6E,QAAQjD,QAAQ1f,MAAM,KAC5C,IAAK,MAAM0f,KAAW4gB,EACpB,GAAgB,UAAZ5gB,EACFrB,GAAac,GAAGrB,KAAK4E,SAAU5E,KAAKmE,YAAYqB,UAjVlC,SAiV4DxF,KAAK6E,QAAQ9K,UAAUqF,IAC/EY,KAAKqiB,6BAA6BjjB,GAC1CsI,QAAQ,SAEb,GA3VU,WA2VN9F,EAA4B,CACrC,MAAM6gB,EAAU7gB,IAAYyd,GAAgBrf,KAAKmE,YAAYqB,UAnV5C,cAmV0ExF,KAAKmE,YAAYqB,UArV5F,WAsVVkd,EAAW9gB,IAAYyd,GAAgBrf,KAAKmE,YAAYqB,UAnV7C,cAmV2ExF,KAAKmE,YAAYqB,UArV5F,YAsVjBjF,GAAac,GAAGrB,KAAK4E,SAAU6d,EAASziB,KAAK6E,QAAQ9K,UAAUqF,IAC7D,MAAM+T,EAAUnT,KAAKqiB,6BAA6BjjB,GAClD+T,EAAQqN,eAA8B,YAAfphB,EAAMqB,KAAqB6e,GAAgBD,KAAiB,EACnFlM,EAAQgO,QAAQ,IAElB5gB,GAAac,GAAGrB,KAAK4E,SAAU8d,EAAU1iB,KAAK6E,QAAQ9K,UAAUqF,IAC9D,MAAM+T,EAAUnT,KAAKqiB,6BAA6BjjB,GAClD+T,EAAQqN,eAA8B,aAAfphB,EAAMqB,KAAsB6e,GAAgBD,IAAiBlM,EAAQvO,SAASpgB,SAAS4a,EAAMU,eACpHqT,EAAQ+N,QAAQ,GAEpB,CAEFlhB,KAAKohB,kBAAoB,KACnBphB,KAAK4E,UACP5E,KAAKyP,MACP,EAEFlP,GAAac,GAAGrB,KAAK4E,SAAS5J,QAAQmkB,IAAiBC,GAAkBpf,KAAKohB,kBAChF,CACA,SAAAP,GACE,MAAMX,EAAQlgB,KAAK4E,SAASpJ,aAAa,SACpC0kB,IAGAlgB,KAAK4E,SAASpJ,aAAa,eAAkBwE,KAAK4E,SAAS0Z,YAAY3Y,QAC1E3F,KAAK4E,SAASxjB,aAAa,aAAc8+B,GAE3ClgB,KAAK4E,SAASxjB,aAAa,yBAA0B8+B,GACrDlgB,KAAK4E,SAASzjB,gBAAgB,SAChC,CACA,MAAAggC,GACMnhB,KAAKwP,YAAcxP,KAAKugB,WAC1BvgB,KAAKugB,YAAa,GAGpBvgB,KAAKugB,YAAa,EAClBvgB,KAAK2iB,aAAY,KACX3iB,KAAKugB,YACPvgB,KAAK0P,MACP,GACC1P,KAAK6E,QAAQob,MAAMvQ,MACxB,CACA,MAAAwR,GACMlhB,KAAKyhB,yBAGTzhB,KAAKugB,YAAa,EAClBvgB,KAAK2iB,aAAY,KACV3iB,KAAKugB,YACRvgB,KAAKyP,MACP,GACCzP,KAAK6E,QAAQob,MAAMxQ,MACxB,CACA,WAAAkT,CAAY/kB,EAASglB,GACnB7V,aAAa/M,KAAKsgB,UAClBtgB,KAAKsgB,SAAWziB,WAAWD,EAASglB,EACtC,CACA,oBAAAnB,GACE,OAAOzkC,OAAOmiB,OAAOa,KAAKwgB,gBAAgBpf,UAAS,EACrD,CACA,UAAAyC,CAAWC,GACT,MAAM+e,EAAiB7f,GAAYG,kBAAkBnD,KAAK4E,UAC1D,IAAK,MAAMke,KAAiB9lC,OAAO4D,KAAKiiC,GAClC7D,GAAsBroB,IAAImsB,WACrBD,EAAeC,GAU1B,OAPAhf,EAAS,IACJ+e,KACmB,iBAAX/e,GAAuBA,EAASA,EAAS,CAAC,GAEvDA,EAAS9D,KAAK+D,gBAAgBD,GAC9BA,EAAS9D,KAAKgE,kBAAkBF,GAChC9D,KAAKiE,iBAAiBH,GACfA,CACT,CACA,iBAAAE,CAAkBF,GAchB,OAbAA,EAAOic,WAAiC,IAArBjc,EAAOic,UAAsB16B,SAAS6G,KAAOwO,GAAWoJ,EAAOic,WACtD,iBAAjBjc,EAAOmc,QAChBnc,EAAOmc,MAAQ,CACbvQ,KAAM5L,EAAOmc,MACbxQ,KAAM3L,EAAOmc,QAGW,iBAAjBnc,EAAOoc,QAChBpc,EAAOoc,MAAQpc,EAAOoc,MAAMrgC,YAEA,iBAAnBikB,EAAOiZ,UAChBjZ,EAAOiZ,QAAUjZ,EAAOiZ,QAAQl9B,YAE3BikB,CACT,CACA,kBAAAwe,GACE,MAAMxe,EAAS,CAAC,EAChB,IAAK,MAAOhnB,EAAKa,KAAUX,OAAOmkB,QAAQnB,KAAK6E,SACzC7E,KAAKmE,YAAYT,QAAQ5mB,KAASa,IACpCmmB,EAAOhnB,GAAOa,GASlB,OANAmmB,EAAO/J,UAAW,EAClB+J,EAAOlC,QAAU,SAKVkC,CACT,CACA,cAAAud,GACMrhB,KAAKgS,UACPhS,KAAKgS,QAAQhZ,UACbgH,KAAKgS,QAAU,MAEbhS,KAAK2gB,MACP3gB,KAAK2gB,IAAIhnB,SACTqG,KAAK2gB,IAAM,KAEf,CAGA,sBAAOlkB,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAO+1B,GAAQ9a,oBAAoBtF,KAAM8D,GAC/C,GAAsB,iBAAXA,EAAX,CAGA,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IAJL,CAKF,GACF,EAOF3H,GAAmBikB,IAcnB,MAGM2C,GAAY,IACb3C,GAAQ1c,QACXqZ,QAAS,GACT/0B,OAAQ,CAAC,EAAG,GACZtJ,UAAW,QACXy+B,SAAU,8IACVvb,QAAS,SAELohB,GAAgB,IACjB5C,GAAQzc,YACXoZ,QAAS,kCAOX,MAAMkG,WAAgB7C,GAEpB,kBAAW1c,GACT,OAAOqf,EACT,CACA,sBAAWpf,GACT,OAAOqf,EACT,CACA,eAAWzmB,GACT,MA7BW,SA8Bb,CAGA,cAAA+kB,GACE,OAAOthB,KAAK0hB,aAAe1hB,KAAKkjB,aAClC,CAGA,sBAAAtB,GACE,MAAO,CACL,kBAAkB5hB,KAAK0hB,YACvB,gBAAoB1hB,KAAKkjB,cAE7B,CACA,WAAAA,GACE,OAAOljB,KAAKyd,yBAAyBzd,KAAK6E,QAAQkY,QACpD,CAGA,sBAAOtgB,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAO44B,GAAQ3d,oBAAoBtF,KAAM8D,GAC/C,GAAsB,iBAAXA,EAAX,CAGA,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IAJL,CAKF,GACF,EAOF3H,GAAmB8mB,IAcnB,MAEME,GAAc,gBAEdC,GAAiB,WAAWD,KAC5BE,GAAc,QAAQF,KACtBG,GAAwB,OAAOH,cAE/BI,GAAsB,SAEtBC,GAAwB,SAExBC,GAAqB,YAGrBC,GAAsB,GAAGD,mBAA+CA,uBAGxEE,GAAY,CAChB37B,OAAQ,KAER47B,WAAY,eACZC,cAAc,EACdt3B,OAAQ,KACRu3B,UAAW,CAAC,GAAK,GAAK,IAElBC,GAAgB,CACpB/7B,OAAQ,gBAER47B,WAAY,SACZC,aAAc,UACdt3B,OAAQ,UACRu3B,UAAW,SAOb,MAAME,WAAkBtf,GACtB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GAGf9D,KAAKikB,aAAe,IAAI/yB,IACxB8O,KAAKkkB,oBAAsB,IAAIhzB,IAC/B8O,KAAKmkB,aAA6D,YAA9Cl/B,iBAAiB+a,KAAK4E,UAAU5Y,UAA0B,KAAOgU,KAAK4E,SAC1F5E,KAAKokB,cAAgB,KACrBpkB,KAAKqkB,UAAY,KACjBrkB,KAAKskB,oBAAsB,CACzBC,gBAAiB,EACjBC,gBAAiB,GAEnBxkB,KAAKykB,SACP,CAGA,kBAAW/gB,GACT,OAAOigB,EACT,CACA,sBAAWhgB,GACT,OAAOogB,EACT,CACA,eAAWxnB,GACT,MAhEW,WAiEb,CAGA,OAAAkoB,GACEzkB,KAAK0kB,mCACL1kB,KAAK2kB,2BACD3kB,KAAKqkB,UACPrkB,KAAKqkB,UAAUO,aAEf5kB,KAAKqkB,UAAYrkB,KAAK6kB,kBAExB,IAAK,MAAMC,KAAW9kB,KAAKkkB,oBAAoB/kB,SAC7Ca,KAAKqkB,UAAUU,QAAQD,EAE3B,CACA,OAAA/f,GACE/E,KAAKqkB,UAAUO,aACfjgB,MAAMI,SACR,CAGA,iBAAAf,CAAkBF,GAShB,OAPAA,EAAOvX,OAASmO,GAAWoJ,EAAOvX,SAAWlH,SAAS6G,KAGtD4X,EAAO8f,WAAa9f,EAAO9b,OAAS,GAAG8b,EAAO9b,oBAAsB8b,EAAO8f,WAC3C,iBAArB9f,EAAOggB,YAChBhgB,EAAOggB,UAAYhgB,EAAOggB,UAAU5hC,MAAM,KAAKY,KAAInF,GAAS4f,OAAOC,WAAW7f,MAEzEmmB,CACT,CACA,wBAAA6gB,GACO3kB,KAAK6E,QAAQgf,eAKlBtjB,GAAaC,IAAIR,KAAK6E,QAAQtY,OAAQ82B,IACtC9iB,GAAac,GAAGrB,KAAK6E,QAAQtY,OAAQ82B,GAAaG,IAAuBpkB,IACvE,MAAM4lB,EAAoBhlB,KAAKkkB,oBAAoB/mC,IAAIiiB,EAAM7S,OAAOtB,MACpE,GAAI+5B,EAAmB,CACrB5lB,EAAMkD,iBACN,MAAM3G,EAAOqE,KAAKmkB,cAAgBvkC,OAC5BmE,EAASihC,EAAkB3gC,UAAY2b,KAAK4E,SAASvgB,UAC3D,GAAIsX,EAAKspB,SAKP,YAJAtpB,EAAKspB,SAAS,CACZtjC,IAAKoC,EACLmhC,SAAU,WAMdvpB,EAAKlQ,UAAY1H,CACnB,KAEJ,CACA,eAAA8gC,GACE,MAAMpjC,EAAU,CACdka,KAAMqE,KAAKmkB,aACXL,UAAW9jB,KAAK6E,QAAQif,UACxBF,WAAY5jB,KAAK6E,QAAQ+e,YAE3B,OAAO,IAAIuB,sBAAqBhkB,GAAWnB,KAAKolB,kBAAkBjkB,IAAU1f,EAC9E,CAGA,iBAAA2jC,CAAkBjkB,GAChB,MAAMkkB,EAAgB/H,GAAStd,KAAKikB,aAAa9mC,IAAI,IAAImgC,EAAM/wB,OAAO4N,MAChEob,EAAW+H,IACftd,KAAKskB,oBAAoBC,gBAAkBjH,EAAM/wB,OAAOlI,UACxD2b,KAAKslB,SAASD,EAAc/H,GAAO,EAE/BkH,GAAmBxkB,KAAKmkB,cAAgB9+B,SAASC,iBAAiBmG,UAClE85B,EAAkBf,GAAmBxkB,KAAKskB,oBAAoBE,gBACpExkB,KAAKskB,oBAAoBE,gBAAkBA,EAC3C,IAAK,MAAMlH,KAASnc,EAAS,CAC3B,IAAKmc,EAAMkI,eAAgB,CACzBxlB,KAAKokB,cAAgB,KACrBpkB,KAAKylB,kBAAkBJ,EAAc/H,IACrC,QACF,CACA,MAAMoI,EAA2BpI,EAAM/wB,OAAOlI,WAAa2b,KAAKskB,oBAAoBC,gBAEpF,GAAIgB,GAAmBG,GAGrB,GAFAnQ,EAAS+H,IAEJkH,EACH,YAMCe,GAAoBG,GACvBnQ,EAAS+H,EAEb,CACF,CACA,gCAAAoH,GACE1kB,KAAKikB,aAAe,IAAI/yB,IACxB8O,KAAKkkB,oBAAsB,IAAIhzB,IAC/B,MAAMy0B,EAAc/f,GAAezT,KAAKqxB,GAAuBxjB,KAAK6E,QAAQtY,QAC5E,IAAK,MAAMq5B,KAAUD,EAAa,CAEhC,IAAKC,EAAO36B,MAAQiQ,GAAW0qB,GAC7B,SAEF,MAAMZ,EAAoBpf,GAAeC,QAAQggB,UAAUD,EAAO36B,MAAO+U,KAAK4E,UAG1EjK,GAAUqqB,KACZhlB,KAAKikB,aAAalyB,IAAI8zB,UAAUD,EAAO36B,MAAO26B,GAC9C5lB,KAAKkkB,oBAAoBnyB,IAAI6zB,EAAO36B,KAAM+5B,GAE9C,CACF,CACA,QAAAM,CAAS/4B,GACHyT,KAAKokB,gBAAkB73B,IAG3ByT,KAAKylB,kBAAkBzlB,KAAK6E,QAAQtY,QACpCyT,KAAKokB,cAAgB73B,EACrBA,EAAO8O,UAAU5E,IAAI8sB,IACrBvjB,KAAK8lB,iBAAiBv5B,GACtBgU,GAAaqB,QAAQ5B,KAAK4E,SAAUwe,GAAgB,CAClDtjB,cAAevT,IAEnB,CACA,gBAAAu5B,CAAiBv5B,GAEf,GAAIA,EAAO8O,UAAU7W,SA9LQ,iBA+L3BohB,GAAeC,QArLc,mBAqLsBtZ,EAAOyO,QAtLtC,cAsLkEK,UAAU5E,IAAI8sB,SAGtG,IAAK,MAAMwC,KAAangB,GAAeI,QAAQzZ,EA9LnB,qBAiM1B,IAAK,MAAMxJ,KAAQ6iB,GAAeM,KAAK6f,EAAWrC,IAChD3gC,EAAKsY,UAAU5E,IAAI8sB,GAGzB,CACA,iBAAAkC,CAAkBhhC,GAChBA,EAAO4W,UAAU1B,OAAO4pB,IACxB,MAAMyC,EAAcpgB,GAAezT,KAAK,GAAGqxB,MAAyBD,KAAuB9+B,GAC3F,IAAK,MAAM9E,KAAQqmC,EACjBrmC,EAAK0b,UAAU1B,OAAO4pB,GAE1B,CAGA,sBAAO9mB,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAO25B,GAAU1e,oBAAoBtF,KAAM8D,GACjD,GAAsB,iBAAXA,EAAX,CAGA,QAAqB/K,IAAjB1O,EAAKyZ,IAAyBA,EAAOrC,WAAW,MAAmB,gBAAXqC,EAC1D,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IAJL,CAKF,GACF,EAOFvD,GAAac,GAAGzhB,OAAQ0jC,IAAuB,KAC7C,IAAK,MAAM2C,KAAOrgB,GAAezT,KApOT,0BAqOtB6xB,GAAU1e,oBAAoB2gB,EAChC,IAOF9pB,GAAmB6nB,IAcnB,MAEMkC,GAAc,UACdC,GAAe,OAAOD,KACtBE,GAAiB,SAASF,KAC1BG,GAAe,OAAOH,KACtBI,GAAgB,QAAQJ,KACxBK,GAAuB,QAAQL,KAC/BM,GAAgB,UAAUN,KAC1BO,GAAsB,OAAOP,KAC7BQ,GAAiB,YACjBC,GAAkB,aAClBC,GAAe,UACfC,GAAiB,YACjBC,GAAW,OACXC,GAAU,MACVC,GAAoB,SACpBC,GAAoB,OACpBC,GAAoB,OAEpBC,GAA2B,mBAE3BC,GAA+B,QAAQD,MAIvCE,GAAuB,2EACvBC,GAAsB,YAFOF,uBAAiDA,mBAA6CA,OAE/EC,KAC5CE,GAA8B,IAAIP,8BAA6CA,+BAA8CA,4BAMnI,MAAMQ,WAAY9iB,GAChB,WAAAP,CAAY5kB,GACVolB,MAAMplB,GACNygB,KAAKiS,QAAUjS,KAAK4E,SAAS5J,QAdN,uCAelBgF,KAAKiS,UAOVjS,KAAKynB,sBAAsBznB,KAAKiS,QAASjS,KAAK0nB,gBAC9CnnB,GAAac,GAAGrB,KAAK4E,SAAU4hB,IAAepnB,GAASY,KAAK0M,SAAStN,KACvE,CAGA,eAAW7C,GACT,MAnDW,KAoDb,CAGA,IAAAmT,GAEE,MAAMiY,EAAY3nB,KAAK4E,SACvB,GAAI5E,KAAK4nB,cAAcD,GACrB,OAIF,MAAME,EAAS7nB,KAAK8nB,iBACdC,EAAYF,EAAStnB,GAAaqB,QAAQimB,EAAQ1B,GAAc,CACpErmB,cAAe6nB,IACZ,KACapnB,GAAaqB,QAAQ+lB,EAAWtB,GAAc,CAC9DvmB,cAAe+nB,IAEH7lB,kBAAoB+lB,GAAaA,EAAU/lB,mBAGzDhC,KAAKgoB,YAAYH,EAAQF,GACzB3nB,KAAKioB,UAAUN,EAAWE,GAC5B,CAGA,SAAAI,CAAU1oC,EAAS2oC,GACZ3oC,IAGLA,EAAQ8b,UAAU5E,IAAIuwB,IACtBhnB,KAAKioB,UAAUriB,GAAec,uBAAuBnnB,IAcrDygB,KAAKmF,gBAZY,KACsB,QAAjC5lB,EAAQic,aAAa,SAIzBjc,EAAQ4B,gBAAgB,YACxB5B,EAAQ6B,aAAa,iBAAiB,GACtC4e,KAAKmoB,gBAAgB5oC,GAAS,GAC9BghB,GAAaqB,QAAQriB,EAAS+mC,GAAe,CAC3CxmB,cAAeooB,KAPf3oC,EAAQ8b,UAAU5E,IAAIywB,GAQtB,GAE0B3nC,EAASA,EAAQ8b,UAAU7W,SAASyiC,KACpE,CACA,WAAAe,CAAYzoC,EAAS2oC,GACd3oC,IAGLA,EAAQ8b,UAAU1B,OAAOqtB,IACzBznC,EAAQm7B,OACR1a,KAAKgoB,YAAYpiB,GAAec,uBAAuBnnB,IAcvDygB,KAAKmF,gBAZY,KACsB,QAAjC5lB,EAAQic,aAAa,SAIzBjc,EAAQ6B,aAAa,iBAAiB,GACtC7B,EAAQ6B,aAAa,WAAY,MACjC4e,KAAKmoB,gBAAgB5oC,GAAS,GAC9BghB,GAAaqB,QAAQriB,EAAS6mC,GAAgB,CAC5CtmB,cAAeooB,KAPf3oC,EAAQ8b,UAAU1B,OAAOutB,GAQzB,GAE0B3nC,EAASA,EAAQ8b,UAAU7W,SAASyiC,KACpE,CACA,QAAAva,CAAStN,GACP,IAAK,CAACsnB,GAAgBC,GAAiBC,GAAcC,GAAgBC,GAAUC,IAAS3lB,SAAShC,EAAMtiB,KACrG,OAEFsiB,EAAMuU,kBACNvU,EAAMkD,iBACN,MAAMwD,EAAW9F,KAAK0nB,eAAevhC,QAAO5G,IAAY2b,GAAW3b,KACnE,IAAI6oC,EACJ,GAAI,CAACtB,GAAUC,IAAS3lB,SAAShC,EAAMtiB,KACrCsrC,EAAoBtiB,EAAS1G,EAAMtiB,MAAQgqC,GAAW,EAAIhhB,EAASpV,OAAS,OACvE,CACL,MAAM2c,EAAS,CAACsZ,GAAiBE,IAAgBzlB,SAAShC,EAAMtiB,KAChEsrC,EAAoBtqB,GAAqBgI,EAAU1G,EAAM7S,OAAQ8gB,GAAQ,EAC3E,CACI+a,IACFA,EAAkB9V,MAAM,CACtB+V,eAAe,IAEjBb,GAAIliB,oBAAoB8iB,GAAmB1Y,OAE/C,CACA,YAAAgY,GAEE,OAAO9hB,GAAezT,KAAKm1B,GAAqBtnB,KAAKiS,QACvD,CACA,cAAA6V,GACE,OAAO9nB,KAAK0nB,eAAev1B,MAAKzN,GAASsb,KAAK4nB,cAAcljC,MAAW,IACzE,CACA,qBAAA+iC,CAAsBhjC,EAAQqhB,GAC5B9F,KAAKsoB,yBAAyB7jC,EAAQ,OAAQ,WAC9C,IAAK,MAAMC,KAASohB,EAClB9F,KAAKuoB,6BAA6B7jC,EAEtC,CACA,4BAAA6jC,CAA6B7jC,GAC3BA,EAAQsb,KAAKwoB,iBAAiB9jC,GAC9B,MAAM+jC,EAAWzoB,KAAK4nB,cAAcljC,GAC9BgkC,EAAY1oB,KAAK2oB,iBAAiBjkC,GACxCA,EAAMtD,aAAa,gBAAiBqnC,GAChCC,IAAchkC,GAChBsb,KAAKsoB,yBAAyBI,EAAW,OAAQ,gBAE9CD,GACH/jC,EAAMtD,aAAa,WAAY,MAEjC4e,KAAKsoB,yBAAyB5jC,EAAO,OAAQ,OAG7Csb,KAAK4oB,mCAAmClkC,EAC1C,CACA,kCAAAkkC,CAAmClkC,GACjC,MAAM6H,EAASqZ,GAAec,uBAAuBhiB,GAChD6H,IAGLyT,KAAKsoB,yBAAyB/7B,EAAQ,OAAQ,YAC1C7H,EAAMyV,IACR6F,KAAKsoB,yBAAyB/7B,EAAQ,kBAAmB,GAAG7H,EAAMyV,MAEtE,CACA,eAAAguB,CAAgB5oC,EAASspC,GACvB,MAAMH,EAAY1oB,KAAK2oB,iBAAiBppC,GACxC,IAAKmpC,EAAUrtB,UAAU7W,SApKN,YAqKjB,OAEF,MAAMkjB,EAAS,CAAC3N,EAAUia,KACxB,MAAMz0B,EAAUqmB,GAAeC,QAAQ9L,EAAU2uB,GAC7CnpC,GACFA,EAAQ8b,UAAUqM,OAAOsM,EAAW6U,EACtC,EAEFnhB,EAAOyf,GAA0BH,IACjCtf,EA5K2B,iBA4KIwf,IAC/BwB,EAAUtnC,aAAa,gBAAiBynC,EAC1C,CACA,wBAAAP,CAAyB/oC,EAASwC,EAAWpE,GACtC4B,EAAQgc,aAAaxZ,IACxBxC,EAAQ6B,aAAaW,EAAWpE,EAEpC,CACA,aAAAiqC,CAAczY,GACZ,OAAOA,EAAK9T,UAAU7W,SAASwiC,GACjC,CAGA,gBAAAwB,CAAiBrZ,GACf,OAAOA,EAAKpJ,QAAQuhB,IAAuBnY,EAAOvJ,GAAeC,QAAQyhB,GAAqBnY,EAChG,CAGA,gBAAAwZ,CAAiBxZ,GACf,OAAOA,EAAKnU,QA5LO,gCA4LoBmU,CACzC,CAGA,sBAAO1S,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAOm9B,GAAIliB,oBAAoBtF,MACrC,GAAsB,iBAAX8D,EAAX,CAGA,QAAqB/K,IAAjB1O,EAAKyZ,IAAyBA,EAAOrC,WAAW,MAAmB,gBAAXqC,EAC1D,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,IAJL,CAKF,GACF,EAOFvD,GAAac,GAAGhc,SAAUkhC,GAAsBc,IAAsB,SAAUjoB,GAC1E,CAAC,IAAK,QAAQgC,SAASpB,KAAKgH,UAC9B5H,EAAMkD,iBAEJpH,GAAW8E,OAGfwnB,GAAIliB,oBAAoBtF,MAAM0P,MAChC,IAKAnP,GAAac,GAAGzhB,OAAQ6mC,IAAqB,KAC3C,IAAK,MAAMlnC,KAAWqmB,GAAezT,KAAKo1B,IACxCC,GAAIliB,oBAAoB/lB,EAC1B,IAMF4c,GAAmBqrB,IAcnB,MAEMxiB,GAAY,YACZ8jB,GAAkB,YAAY9jB,KAC9B+jB,GAAiB,WAAW/jB,KAC5BgkB,GAAgB,UAAUhkB,KAC1BikB,GAAiB,WAAWjkB,KAC5BkkB,GAAa,OAAOlkB,KACpBmkB,GAAe,SAASnkB,KACxBokB,GAAa,OAAOpkB,KACpBqkB,GAAc,QAAQrkB,KAEtBskB,GAAkB,OAClBC,GAAkB,OAClBC,GAAqB,UACrB7lB,GAAc,CAClBmc,UAAW,UACX2J,SAAU,UACVxJ,MAAO,UAEHvc,GAAU,CACdoc,WAAW,EACX2J,UAAU,EACVxJ,MAAO,KAOT,MAAMyJ,WAAchlB,GAClB,WAAAP,CAAY5kB,EAASukB,GACnBa,MAAMplB,EAASukB,GACf9D,KAAKsgB,SAAW,KAChBtgB,KAAK2pB,sBAAuB,EAC5B3pB,KAAK4pB,yBAA0B,EAC/B5pB,KAAK4gB,eACP,CAGA,kBAAWld,GACT,OAAOA,EACT,CACA,sBAAWC,GACT,OAAOA,EACT,CACA,eAAWpH,GACT,MA/CS,OAgDX,CAGA,IAAAmT,GACoBnP,GAAaqB,QAAQ5B,KAAK4E,SAAUwkB,IACxCpnB,mBAGdhC,KAAK6pB,gBACD7pB,KAAK6E,QAAQib,WACf9f,KAAK4E,SAASvJ,UAAU5E,IA/CN,QAsDpBuJ,KAAK4E,SAASvJ,UAAU1B,OAAO2vB,IAC/BztB,GAAOmE,KAAK4E,UACZ5E,KAAK4E,SAASvJ,UAAU5E,IAAI8yB,GAAiBC,IAC7CxpB,KAAKmF,gBARY,KACfnF,KAAK4E,SAASvJ,UAAU1B,OAAO6vB,IAC/BjpB,GAAaqB,QAAQ5B,KAAK4E,SAAUykB,IACpCrpB,KAAK8pB,oBAAoB,GAKG9pB,KAAK4E,SAAU5E,KAAK6E,QAAQib,WAC5D,CACA,IAAArQ,GACOzP,KAAK+pB,YAGQxpB,GAAaqB,QAAQ5B,KAAK4E,SAAUskB,IACxClnB,mBAQdhC,KAAK4E,SAASvJ,UAAU5E,IAAI+yB,IAC5BxpB,KAAKmF,gBANY,KACfnF,KAAK4E,SAASvJ,UAAU5E,IAAI6yB,IAC5BtpB,KAAK4E,SAASvJ,UAAU1B,OAAO6vB,GAAoBD,IACnDhpB,GAAaqB,QAAQ5B,KAAK4E,SAAUukB,GAAa,GAGrBnpB,KAAK4E,SAAU5E,KAAK6E,QAAQib,YAC5D,CACA,OAAA/a,GACE/E,KAAK6pB,gBACD7pB,KAAK+pB,WACP/pB,KAAK4E,SAASvJ,UAAU1B,OAAO4vB,IAEjC5kB,MAAMI,SACR,CACA,OAAAglB,GACE,OAAO/pB,KAAK4E,SAASvJ,UAAU7W,SAAS+kC,GAC1C,CAIA,kBAAAO,GACO9pB,KAAK6E,QAAQ4kB,WAGdzpB,KAAK2pB,sBAAwB3pB,KAAK4pB,0BAGtC5pB,KAAKsgB,SAAWziB,YAAW,KACzBmC,KAAKyP,MAAM,GACVzP,KAAK6E,QAAQob,QAClB,CACA,cAAA+J,CAAe5qB,EAAO6qB,GACpB,OAAQ7qB,EAAMqB,MACZ,IAAK,YACL,IAAK,WAEDT,KAAK2pB,qBAAuBM,EAC5B,MAEJ,IAAK,UACL,IAAK,WAEDjqB,KAAK4pB,wBAA0BK,EAIrC,GAAIA,EAEF,YADAjqB,KAAK6pB,gBAGP,MAAMvc,EAAclO,EAAMU,cACtBE,KAAK4E,WAAa0I,GAAetN,KAAK4E,SAASpgB,SAAS8oB,IAG5DtN,KAAK8pB,oBACP,CACA,aAAAlJ,GACErgB,GAAac,GAAGrB,KAAK4E,SAAUkkB,IAAiB1pB,GAASY,KAAKgqB,eAAe5qB,GAAO,KACpFmB,GAAac,GAAGrB,KAAK4E,SAAUmkB,IAAgB3pB,GAASY,KAAKgqB,eAAe5qB,GAAO,KACnFmB,GAAac,GAAGrB,KAAK4E,SAAUokB,IAAe5pB,GAASY,KAAKgqB,eAAe5qB,GAAO,KAClFmB,GAAac,GAAGrB,KAAK4E,SAAUqkB,IAAgB7pB,GAASY,KAAKgqB,eAAe5qB,GAAO,IACrF,CACA,aAAAyqB,GACE9c,aAAa/M,KAAKsgB,UAClBtgB,KAAKsgB,SAAW,IAClB,CAGA,sBAAO7jB,CAAgBqH,GACrB,OAAO9D,KAAKuH,MAAK,WACf,MAAMld,EAAOq/B,GAAMpkB,oBAAoBtF,KAAM8D,GAC7C,GAAsB,iBAAXA,EAAqB,CAC9B,QAA4B,IAAjBzZ,EAAKyZ,GACd,MAAM,IAAIU,UAAU,oBAAoBV,MAE1CzZ,EAAKyZ,GAAQ9D,KACf,CACF,GACF,ECr0IK,SAASkqB,GAAc7tB,GACD,WAAvBhX,SAASuX,WAAyBP,IACjChX,SAASyF,iBAAiB,mBAAoBuR,EACrD,CDy0IAuK,GAAqB8iB,IAMrBvtB,GAAmButB,IEpyInBQ,IAzCA,WAC2B,GAAG93B,MAAM5U,KAChC6H,SAAS+a,iBAAiB,+BAETtd,KAAI,SAAUqnC,GAC/B,OAAO,IAAI,GAAkBA,EAAkB,CAC7ClK,MAAO,CAAEvQ,KAAM,IAAKD,KAAM,MAE9B,GACF,IAiCAya,IA5BA,WACY7kC,SAAS68B,eAAe,mBAC9Bp3B,iBAAiB,SAAS,WAC5BzF,SAAS6G,KAAKT,UAAY,EAC1BpG,SAASC,gBAAgBmG,UAAY,CACvC,GACF,IAuBAy+B,IArBA,WACE,IAAIE,EAAM/kC,SAAS68B,eAAe,mBAC9BmI,EAAShlC,SACVilC,uBAAuB,aAAa,GACpChnC,wBACH1D,OAAOkL,iBAAiB,UAAU,WAC5BkV,KAAKuqB,UAAYvqB,KAAKwqB,SAAWxqB,KAAKwqB,QAAUH,EAAOzsC,OACzDwsC,EAAIrpC,MAAM6wB,QAAU,QAEpBwY,EAAIrpC,MAAM6wB,QAAU,OAEtB5R,KAAKuqB,UAAYvqB,KAAKwqB,OACxB,GACF,IAUA5qC,OAAO6qC,UAAY","sources":["webpack://pydata_sphinx_theme/webpack/bootstrap","webpack://pydata_sphinx_theme/webpack/runtime/define property getters","webpack://pydata_sphinx_theme/webpack/runtime/hasOwnProperty shorthand","webpack://pydata_sphinx_theme/webpack/runtime/make namespace object","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/enums.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getNodeName.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/instanceOf.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/applyStyles.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getBasePlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/math.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/userAgent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isLayoutViewport.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getBoundingClientRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getLayoutRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/contains.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getComputedStyle.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isTableElement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getDocumentElement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getParentNode.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getOffsetParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getMainAxisFromPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/within.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/mergePaddingObject.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getFreshSideObject.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/expandToHashMap.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/arrow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getVariation.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/computeStyles.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/eventListeners.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getOppositePlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindowScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getWindowScrollBarX.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/isScrollParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getScrollParent.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/listScrollParents.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/rectToClientRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getClippingRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getViewportRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getDocumentRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/computeOffsets.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/detectOverflow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/flip.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/computeAutoPlacement.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/hide.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/offset.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/popperOffsets.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/modifiers/preventOverflow.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/getAltAxis.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getCompositeRect.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getNodeScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/dom-utils/getHTMLElementScroll.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/orderModifiers.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/createPopper.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/debounce.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/utils/mergeByName.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/popper.js","webpack://pydata_sphinx_theme/./node_modules/@popperjs/core/lib/popper-lite.js","webpack://pydata_sphinx_theme/./node_modules/bootstrap/dist/js/bootstrap.esm.js","webpack://pydata_sphinx_theme/./src/pydata_sphinx_theme/assets/scripts/mixin.js","webpack://pydata_sphinx_theme/./src/pydata_sphinx_theme/assets/scripts/bootstrap.js"],"sourcesContent":["// The require scope\nvar __webpack_require__ = {};\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","export var top = 'top';\nexport var bottom = 'bottom';\nexport var right = 'right';\nexport var left = 'left';\nexport var auto = 'auto';\nexport var basePlacements = [top, bottom, right, left];\nexport var start = 'start';\nexport var end = 'end';\nexport var clippingParents = 'clippingParents';\nexport var viewport = 'viewport';\nexport var popper = 'popper';\nexport var reference = 'reference';\nexport var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) {\n return acc.concat([placement + \"-\" + start, placement + \"-\" + end]);\n}, []);\nexport var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) {\n return acc.concat([placement, placement + \"-\" + start, placement + \"-\" + end]);\n}, []); // modifiers that need to read the DOM\n\nexport var beforeRead = 'beforeRead';\nexport var read = 'read';\nexport var afterRead = 'afterRead'; // pure-logic modifiers\n\nexport var beforeMain = 'beforeMain';\nexport var main = 'main';\nexport var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state)\n\nexport var beforeWrite = 'beforeWrite';\nexport var write = 'write';\nexport var afterWrite = 'afterWrite';\nexport var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite];","export default function getNodeName(element) {\n return element ? (element.nodeName || '').toLowerCase() : null;\n}","export default function getWindow(node) {\n if (node == null) {\n return window;\n }\n\n if (node.toString() !== '[object Window]') {\n var ownerDocument = node.ownerDocument;\n return ownerDocument ? ownerDocument.defaultView || window : window;\n }\n\n return node;\n}","import getWindow from \"./getWindow.js\";\n\nfunction isElement(node) {\n var OwnElement = getWindow(node).Element;\n return node instanceof OwnElement || node instanceof Element;\n}\n\nfunction isHTMLElement(node) {\n var OwnElement = getWindow(node).HTMLElement;\n return node instanceof OwnElement || node instanceof HTMLElement;\n}\n\nfunction isShadowRoot(node) {\n // IE 11 has no ShadowRoot\n if (typeof ShadowRoot === 'undefined') {\n return false;\n }\n\n var OwnElement = getWindow(node).ShadowRoot;\n return node instanceof OwnElement || node instanceof ShadowRoot;\n}\n\nexport { isElement, isHTMLElement, isShadowRoot };","import getNodeName from \"../dom-utils/getNodeName.js\";\nimport { isHTMLElement } from \"../dom-utils/instanceOf.js\"; // This modifier takes the styles prepared by the `computeStyles` modifier\n// and applies them to the HTMLElements such as popper and arrow\n\nfunction applyStyles(_ref) {\n var state = _ref.state;\n Object.keys(state.elements).forEach(function (name) {\n var style = state.styles[name] || {};\n var attributes = state.attributes[name] || {};\n var element = state.elements[name]; // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n } // Flow doesn't support to extend this property, but it's the most\n // effective way to apply styles to an HTMLElement\n // $FlowFixMe[cannot-write]\n\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (name) {\n var value = attributes[name];\n\n if (value === false) {\n element.removeAttribute(name);\n } else {\n element.setAttribute(name, value === true ? '' : value);\n }\n });\n });\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state;\n var initialStyles = {\n popper: {\n position: state.options.strategy,\n left: '0',\n top: '0',\n margin: '0'\n },\n arrow: {\n position: 'absolute'\n },\n reference: {}\n };\n Object.assign(state.elements.popper.style, initialStyles.popper);\n state.styles = initialStyles;\n\n if (state.elements.arrow) {\n Object.assign(state.elements.arrow.style, initialStyles.arrow);\n }\n\n return function () {\n Object.keys(state.elements).forEach(function (name) {\n var element = state.elements[name];\n var attributes = state.attributes[name] || {};\n var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them\n\n var style = styleProperties.reduce(function (style, property) {\n style[property] = '';\n return style;\n }, {}); // arrow is optional + virtual elements\n\n if (!isHTMLElement(element) || !getNodeName(element)) {\n return;\n }\n\n Object.assign(element.style, style);\n Object.keys(attributes).forEach(function (attribute) {\n element.removeAttribute(attribute);\n });\n });\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'applyStyles',\n enabled: true,\n phase: 'write',\n fn: applyStyles,\n effect: effect,\n requires: ['computeStyles']\n};","import { auto } from \"../enums.js\";\nexport default function getBasePlacement(placement) {\n return placement.split('-')[0];\n}","export var max = Math.max;\nexport var min = Math.min;\nexport var round = Math.round;","export default function getUAString() {\n var uaData = navigator.userAgentData;\n\n if (uaData != null && uaData.brands && Array.isArray(uaData.brands)) {\n return uaData.brands.map(function (item) {\n return item.brand + \"/\" + item.version;\n }).join(' ');\n }\n\n return navigator.userAgent;\n}","import getUAString from \"../utils/userAgent.js\";\nexport default function isLayoutViewport() {\n return !/^((?!chrome|android).)*safari/i.test(getUAString());\n}","import { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport { round } from \"../utils/math.js\";\nimport getWindow from \"./getWindow.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getBoundingClientRect(element, includeScale, isFixedStrategy) {\n if (includeScale === void 0) {\n includeScale = false;\n }\n\n if (isFixedStrategy === void 0) {\n isFixedStrategy = false;\n }\n\n var clientRect = element.getBoundingClientRect();\n var scaleX = 1;\n var scaleY = 1;\n\n if (includeScale && isHTMLElement(element)) {\n scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1;\n scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1;\n }\n\n var _ref = isElement(element) ? getWindow(element) : window,\n visualViewport = _ref.visualViewport;\n\n var addVisualOffsets = !isLayoutViewport() && isFixedStrategy;\n var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX;\n var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY;\n var width = clientRect.width / scaleX;\n var height = clientRect.height / scaleY;\n return {\n width: width,\n height: height,\n top: y,\n right: x + width,\n bottom: y + height,\n left: x,\n x: x,\n y: y\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\"; // Returns the layout rect of an element relative to its offsetParent. Layout\n// means it doesn't take into account transforms.\n\nexport default function getLayoutRect(element) {\n var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed.\n // Fixes https://github.com/popperjs/popper-core/issues/1223\n\n var width = element.offsetWidth;\n var height = element.offsetHeight;\n\n if (Math.abs(clientRect.width - width) <= 1) {\n width = clientRect.width;\n }\n\n if (Math.abs(clientRect.height - height) <= 1) {\n height = clientRect.height;\n }\n\n return {\n x: element.offsetLeft,\n y: element.offsetTop,\n width: width,\n height: height\n };\n}","import { isShadowRoot } from \"./instanceOf.js\";\nexport default function contains(parent, child) {\n var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method\n\n if (parent.contains(child)) {\n return true;\n } // then fallback to custom implementation with Shadow DOM support\n else if (rootNode && isShadowRoot(rootNode)) {\n var next = child;\n\n do {\n if (next && parent.isSameNode(next)) {\n return true;\n } // $FlowFixMe[prop-missing]: need a better way to handle this...\n\n\n next = next.parentNode || next.host;\n } while (next);\n } // Give up, the result is false\n\n\n return false;\n}","import getWindow from \"./getWindow.js\";\nexport default function getComputedStyle(element) {\n return getWindow(element).getComputedStyle(element);\n}","import getNodeName from \"./getNodeName.js\";\nexport default function isTableElement(element) {\n return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0;\n}","import { isElement } from \"./instanceOf.js\";\nexport default function getDocumentElement(element) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return ((isElement(element) ? element.ownerDocument : // $FlowFixMe[prop-missing]\n element.document) || window.document).documentElement;\n}","import getNodeName from \"./getNodeName.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport { isShadowRoot } from \"./instanceOf.js\";\nexport default function getParentNode(element) {\n if (getNodeName(element) === 'html') {\n return element;\n }\n\n return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle\n // $FlowFixMe[incompatible-return]\n // $FlowFixMe[prop-missing]\n element.assignedSlot || // step into the shadow DOM of the parent of a slotted node\n element.parentNode || ( // DOM Element detected\n isShadowRoot(element) ? element.host : null) || // ShadowRoot detected\n // $FlowFixMe[incompatible-call]: HTMLElement is a Node\n getDocumentElement(element) // fallback\n\n );\n}","import getWindow from \"./getWindow.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isHTMLElement, isShadowRoot } from \"./instanceOf.js\";\nimport isTableElement from \"./isTableElement.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getUAString from \"../utils/userAgent.js\";\n\nfunction getTrueOffsetParent(element) {\n if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837\n getComputedStyle(element).position === 'fixed') {\n return null;\n }\n\n return element.offsetParent;\n} // `.offsetParent` reports `null` for fixed elements, while absolute elements\n// return the containing block\n\n\nfunction getContainingBlock(element) {\n var isFirefox = /firefox/i.test(getUAString());\n var isIE = /Trident/i.test(getUAString());\n\n if (isIE && isHTMLElement(element)) {\n // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport\n var elementCss = getComputedStyle(element);\n\n if (elementCss.position === 'fixed') {\n return null;\n }\n }\n\n var currentNode = getParentNode(element);\n\n if (isShadowRoot(currentNode)) {\n currentNode = currentNode.host;\n }\n\n while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {\n var css = getComputedStyle(currentNode); // This is non-exhaustive but covers the most common CSS properties that\n // create a containing block.\n // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block\n\n if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') {\n return currentNode;\n } else {\n currentNode = currentNode.parentNode;\n }\n }\n\n return null;\n} // Gets the closest ancestor positioned element. Handles some edge cases,\n// such as table ancestors and cross browser bugs.\n\n\nexport default function getOffsetParent(element) {\n var window = getWindow(element);\n var offsetParent = getTrueOffsetParent(element);\n\n while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === 'static') {\n offsetParent = getTrueOffsetParent(offsetParent);\n }\n\n if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle(offsetParent).position === 'static')) {\n return window;\n }\n\n return offsetParent || getContainingBlock(element) || window;\n}","export default function getMainAxisFromPlacement(placement) {\n return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y';\n}","import { max as mathMax, min as mathMin } from \"./math.js\";\nexport function within(min, value, max) {\n return mathMax(min, mathMin(value, max));\n}\nexport function withinMaxClamp(min, value, max) {\n var v = within(min, value, max);\n return v > max ? max : v;\n}","import getFreshSideObject from \"./getFreshSideObject.js\";\nexport default function mergePaddingObject(paddingObject) {\n return Object.assign({}, getFreshSideObject(), paddingObject);\n}","export default function getFreshSideObject() {\n return {\n top: 0,\n right: 0,\n bottom: 0,\n left: 0\n };\n}","export default function expandToHashMap(value, keys) {\n return keys.reduce(function (hashMap, key) {\n hashMap[key] = value;\n return hashMap;\n }, {});\n}","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport contains from \"../dom-utils/contains.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport { within } from \"../utils/within.js\";\nimport mergePaddingObject from \"../utils/mergePaddingObject.js\";\nimport expandToHashMap from \"../utils/expandToHashMap.js\";\nimport { left, right, basePlacements, top, bottom } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar toPaddingObject = function toPaddingObject(padding, state) {\n padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, {\n placement: state.placement\n })) : padding;\n return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n};\n\nfunction arrow(_ref) {\n var _state$modifiersData$;\n\n var state = _ref.state,\n name = _ref.name,\n options = _ref.options;\n var arrowElement = state.elements.arrow;\n var popperOffsets = state.modifiersData.popperOffsets;\n var basePlacement = getBasePlacement(state.placement);\n var axis = getMainAxisFromPlacement(basePlacement);\n var isVertical = [left, right].indexOf(basePlacement) >= 0;\n var len = isVertical ? 'height' : 'width';\n\n if (!arrowElement || !popperOffsets) {\n return;\n }\n\n var paddingObject = toPaddingObject(options.padding, state);\n var arrowRect = getLayoutRect(arrowElement);\n var minProp = axis === 'y' ? top : left;\n var maxProp = axis === 'y' ? bottom : right;\n var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len];\n var startDiff = popperOffsets[axis] - state.rects.reference[axis];\n var arrowOffsetParent = getOffsetParent(arrowElement);\n var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0;\n var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is\n // outside of the popper bounds\n\n var min = paddingObject[minProp];\n var max = clientSize - arrowRect[len] - paddingObject[maxProp];\n var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference;\n var offset = within(min, center, max); // Prevents breaking syntax highlighting...\n\n var axisProp = axis;\n state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$);\n}\n\nfunction effect(_ref2) {\n var state = _ref2.state,\n options = _ref2.options;\n var _options$element = options.element,\n arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element;\n\n if (arrowElement == null) {\n return;\n } // CSS selector\n\n\n if (typeof arrowElement === 'string') {\n arrowElement = state.elements.popper.querySelector(arrowElement);\n\n if (!arrowElement) {\n return;\n }\n }\n\n if (!contains(state.elements.popper, arrowElement)) {\n return;\n }\n\n state.elements.arrow = arrowElement;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'arrow',\n enabled: true,\n phase: 'main',\n fn: arrow,\n effect: effect,\n requires: ['popperOffsets'],\n requiresIfExists: ['preventOverflow']\n};","export default function getVariation(placement) {\n return placement.split('-')[1];\n}","import { top, left, right, bottom, end } from \"../enums.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport getWindow from \"../dom-utils/getWindow.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getComputedStyle from \"../dom-utils/getComputedStyle.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport { round } from \"../utils/math.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar unsetSides = {\n top: 'auto',\n right: 'auto',\n bottom: 'auto',\n left: 'auto'\n}; // Round the offsets to the nearest suitable subpixel based on the DPR.\n// Zooming can change the DPR, but it seems to report a value that will\n// cleanly divide the values into the appropriate subpixels.\n\nfunction roundOffsetsByDPR(_ref, win) {\n var x = _ref.x,\n y = _ref.y;\n var dpr = win.devicePixelRatio || 1;\n return {\n x: round(x * dpr) / dpr || 0,\n y: round(y * dpr) / dpr || 0\n };\n}\n\nexport function mapToStyles(_ref2) {\n var _Object$assign2;\n\n var popper = _ref2.popper,\n popperRect = _ref2.popperRect,\n placement = _ref2.placement,\n variation = _ref2.variation,\n offsets = _ref2.offsets,\n position = _ref2.position,\n gpuAcceleration = _ref2.gpuAcceleration,\n adaptive = _ref2.adaptive,\n roundOffsets = _ref2.roundOffsets,\n isFixed = _ref2.isFixed;\n var _offsets$x = offsets.x,\n x = _offsets$x === void 0 ? 0 : _offsets$x,\n _offsets$y = offsets.y,\n y = _offsets$y === void 0 ? 0 : _offsets$y;\n\n var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({\n x: x,\n y: y\n }) : {\n x: x,\n y: y\n };\n\n x = _ref3.x;\n y = _ref3.y;\n var hasX = offsets.hasOwnProperty('x');\n var hasY = offsets.hasOwnProperty('y');\n var sideX = left;\n var sideY = top;\n var win = window;\n\n if (adaptive) {\n var offsetParent = getOffsetParent(popper);\n var heightProp = 'clientHeight';\n var widthProp = 'clientWidth';\n\n if (offsetParent === getWindow(popper)) {\n offsetParent = getDocumentElement(popper);\n\n if (getComputedStyle(offsetParent).position !== 'static' && position === 'absolute') {\n heightProp = 'scrollHeight';\n widthProp = 'scrollWidth';\n }\n } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it\n\n\n offsetParent = offsetParent;\n\n if (placement === top || (placement === left || placement === right) && variation === end) {\n sideY = bottom;\n var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]\n offsetParent[heightProp];\n y -= offsetY - popperRect.height;\n y *= gpuAcceleration ? 1 : -1;\n }\n\n if (placement === left || (placement === top || placement === bottom) && variation === end) {\n sideX = right;\n var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]\n offsetParent[widthProp];\n x -= offsetX - popperRect.width;\n x *= gpuAcceleration ? 1 : -1;\n }\n }\n\n var commonStyles = Object.assign({\n position: position\n }, adaptive && unsetSides);\n\n var _ref4 = roundOffsets === true ? roundOffsetsByDPR({\n x: x,\n y: y\n }, getWindow(popper)) : {\n x: x,\n y: y\n };\n\n x = _ref4.x;\n y = _ref4.y;\n\n if (gpuAcceleration) {\n var _Object$assign;\n\n return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? \"translate(\" + x + \"px, \" + y + \"px)\" : \"translate3d(\" + x + \"px, \" + y + \"px, 0)\", _Object$assign));\n }\n\n return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + \"px\" : '', _Object$assign2[sideX] = hasX ? x + \"px\" : '', _Object$assign2.transform = '', _Object$assign2));\n}\n\nfunction computeStyles(_ref5) {\n var state = _ref5.state,\n options = _ref5.options;\n var _options$gpuAccelerat = options.gpuAcceleration,\n gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat,\n _options$adaptive = options.adaptive,\n adaptive = _options$adaptive === void 0 ? true : _options$adaptive,\n _options$roundOffsets = options.roundOffsets,\n roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets;\n var commonStyles = {\n placement: getBasePlacement(state.placement),\n variation: getVariation(state.placement),\n popper: state.elements.popper,\n popperRect: state.rects.popper,\n gpuAcceleration: gpuAcceleration,\n isFixed: state.options.strategy === 'fixed'\n };\n\n if (state.modifiersData.popperOffsets != null) {\n state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.popperOffsets,\n position: state.options.strategy,\n adaptive: adaptive,\n roundOffsets: roundOffsets\n })));\n }\n\n if (state.modifiersData.arrow != null) {\n state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, {\n offsets: state.modifiersData.arrow,\n position: 'absolute',\n adaptive: false,\n roundOffsets: roundOffsets\n })));\n }\n\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-placement': state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'computeStyles',\n enabled: true,\n phase: 'beforeWrite',\n fn: computeStyles,\n data: {}\n};","import getWindow from \"../dom-utils/getWindow.js\"; // eslint-disable-next-line import/no-unused-modules\n\nvar passive = {\n passive: true\n};\n\nfunction effect(_ref) {\n var state = _ref.state,\n instance = _ref.instance,\n options = _ref.options;\n var _options$scroll = options.scroll,\n scroll = _options$scroll === void 0 ? true : _options$scroll,\n _options$resize = options.resize,\n resize = _options$resize === void 0 ? true : _options$resize;\n var window = getWindow(state.elements.popper);\n var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper);\n\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.addEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.addEventListener('resize', instance.update, passive);\n }\n\n return function () {\n if (scroll) {\n scrollParents.forEach(function (scrollParent) {\n scrollParent.removeEventListener('scroll', instance.update, passive);\n });\n }\n\n if (resize) {\n window.removeEventListener('resize', instance.update, passive);\n }\n };\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'eventListeners',\n enabled: true,\n phase: 'write',\n fn: function fn() {},\n effect: effect,\n data: {}\n};","var hash = {\n left: 'right',\n right: 'left',\n bottom: 'top',\n top: 'bottom'\n};\nexport default function getOppositePlacement(placement) {\n return placement.replace(/left|right|bottom|top/g, function (matched) {\n return hash[matched];\n });\n}","var hash = {\n start: 'end',\n end: 'start'\n};\nexport default function getOppositeVariationPlacement(placement) {\n return placement.replace(/start|end/g, function (matched) {\n return hash[matched];\n });\n}","import getWindow from \"./getWindow.js\";\nexport default function getWindowScroll(node) {\n var win = getWindow(node);\n var scrollLeft = win.pageXOffset;\n var scrollTop = win.pageYOffset;\n return {\n scrollLeft: scrollLeft,\n scrollTop: scrollTop\n };\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nexport default function getWindowScrollBarX(element) {\n // If has a CSS width greater than the viewport, then this will be\n // incorrect for RTL.\n // Popper 1 is broken in this case and never had a bug report so let's assume\n // it's not an issue. I don't think anyone ever specifies width on \n // anyway.\n // Browsers where the left scrollbar doesn't cause an issue report `0` for\n // this (e.g. Edge 2019, IE11, Safari)\n return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft;\n}","import getComputedStyle from \"./getComputedStyle.js\";\nexport default function isScrollParent(element) {\n // Firefox wants us to check `-x` and `-y` variations as well\n var _getComputedStyle = getComputedStyle(element),\n overflow = _getComputedStyle.overflow,\n overflowX = _getComputedStyle.overflowX,\n overflowY = _getComputedStyle.overflowY;\n\n return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX);\n}","import getParentNode from \"./getParentNode.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nexport default function getScrollParent(node) {\n if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) {\n // $FlowFixMe[incompatible-return]: assume body is always available\n return node.ownerDocument.body;\n }\n\n if (isHTMLElement(node) && isScrollParent(node)) {\n return node;\n }\n\n return getScrollParent(getParentNode(node));\n}","import getScrollParent from \"./getScrollParent.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport getWindow from \"./getWindow.js\";\nimport isScrollParent from \"./isScrollParent.js\";\n/*\ngiven a DOM element, return the list of all scroll parents, up the list of ancesors\nuntil we get to the top window object. This list is what we attach scroll listeners\nto, because if any of these parent elements scroll, we'll need to re-calculate the\nreference element's position.\n*/\n\nexport default function listScrollParents(element, list) {\n var _element$ownerDocumen;\n\n if (list === void 0) {\n list = [];\n }\n\n var scrollParent = getScrollParent(element);\n var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body);\n var win = getWindow(scrollParent);\n var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent;\n var updatedList = list.concat(target);\n return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here\n updatedList.concat(listScrollParents(getParentNode(target)));\n}","export default function rectToClientRect(rect) {\n return Object.assign({}, rect, {\n left: rect.x,\n top: rect.y,\n right: rect.x + rect.width,\n bottom: rect.y + rect.height\n });\n}","import { viewport } from \"../enums.js\";\nimport getViewportRect from \"./getViewportRect.js\";\nimport getDocumentRect from \"./getDocumentRect.js\";\nimport listScrollParents from \"./listScrollParents.js\";\nimport getOffsetParent from \"./getOffsetParent.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport { isElement, isHTMLElement } from \"./instanceOf.js\";\nimport getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getParentNode from \"./getParentNode.js\";\nimport contains from \"./contains.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport rectToClientRect from \"../utils/rectToClientRect.js\";\nimport { max, min } from \"../utils/math.js\";\n\nfunction getInnerBoundingClientRect(element, strategy) {\n var rect = getBoundingClientRect(element, false, strategy === 'fixed');\n rect.top = rect.top + element.clientTop;\n rect.left = rect.left + element.clientLeft;\n rect.bottom = rect.top + element.clientHeight;\n rect.right = rect.left + element.clientWidth;\n rect.width = element.clientWidth;\n rect.height = element.clientHeight;\n rect.x = rect.left;\n rect.y = rect.top;\n return rect;\n}\n\nfunction getClientRectFromMixedType(element, clippingParent, strategy) {\n return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element)));\n} // A \"clipping parent\" is an overflowable container with the characteristic of\n// clipping (or hiding) overflowing elements with a position different from\n// `initial`\n\n\nfunction getClippingParents(element) {\n var clippingParents = listScrollParents(getParentNode(element));\n var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle(element).position) >= 0;\n var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element;\n\n if (!isElement(clipperElement)) {\n return [];\n } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414\n\n\n return clippingParents.filter(function (clippingParent) {\n return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body';\n });\n} // Gets the maximum area that the element is visible in due to any number of\n// clipping parents\n\n\nexport default function getClippingRect(element, boundary, rootBoundary, strategy) {\n var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary);\n var clippingParents = [].concat(mainClippingParents, [rootBoundary]);\n var firstClippingParent = clippingParents[0];\n var clippingRect = clippingParents.reduce(function (accRect, clippingParent) {\n var rect = getClientRectFromMixedType(element, clippingParent, strategy);\n accRect.top = max(rect.top, accRect.top);\n accRect.right = min(rect.right, accRect.right);\n accRect.bottom = min(rect.bottom, accRect.bottom);\n accRect.left = max(rect.left, accRect.left);\n return accRect;\n }, getClientRectFromMixedType(element, firstClippingParent, strategy));\n clippingRect.width = clippingRect.right - clippingRect.left;\n clippingRect.height = clippingRect.bottom - clippingRect.top;\n clippingRect.x = clippingRect.left;\n clippingRect.y = clippingRect.top;\n return clippingRect;\n}","import getWindow from \"./getWindow.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport isLayoutViewport from \"./isLayoutViewport.js\";\nexport default function getViewportRect(element, strategy) {\n var win = getWindow(element);\n var html = getDocumentElement(element);\n var visualViewport = win.visualViewport;\n var width = html.clientWidth;\n var height = html.clientHeight;\n var x = 0;\n var y = 0;\n\n if (visualViewport) {\n width = visualViewport.width;\n height = visualViewport.height;\n var layoutViewport = isLayoutViewport();\n\n if (layoutViewport || !layoutViewport && strategy === 'fixed') {\n x = visualViewport.offsetLeft;\n y = visualViewport.offsetTop;\n }\n }\n\n return {\n width: width,\n height: height,\n x: x + getWindowScrollBarX(element),\n y: y\n };\n}","import getDocumentElement from \"./getDocumentElement.js\";\nimport getComputedStyle from \"./getComputedStyle.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getWindowScroll from \"./getWindowScroll.js\";\nimport { max } from \"../utils/math.js\"; // Gets the entire size of the scrollable document area, even extending outside\n// of the `` and `` rect bounds if horizontally scrollable\n\nexport default function getDocumentRect(element) {\n var _element$ownerDocumen;\n\n var html = getDocumentElement(element);\n var winScroll = getWindowScroll(element);\n var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body;\n var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0);\n var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0);\n var x = -winScroll.scrollLeft + getWindowScrollBarX(element);\n var y = -winScroll.scrollTop;\n\n if (getComputedStyle(body || html).direction === 'rtl') {\n x += max(html.clientWidth, body ? body.clientWidth : 0) - width;\n }\n\n return {\n width: width,\n height: height,\n x: x,\n y: y\n };\n}","import getBasePlacement from \"./getBasePlacement.js\";\nimport getVariation from \"./getVariation.js\";\nimport getMainAxisFromPlacement from \"./getMainAxisFromPlacement.js\";\nimport { top, right, bottom, left, start, end } from \"../enums.js\";\nexport default function computeOffsets(_ref) {\n var reference = _ref.reference,\n element = _ref.element,\n placement = _ref.placement;\n var basePlacement = placement ? getBasePlacement(placement) : null;\n var variation = placement ? getVariation(placement) : null;\n var commonX = reference.x + reference.width / 2 - element.width / 2;\n var commonY = reference.y + reference.height / 2 - element.height / 2;\n var offsets;\n\n switch (basePlacement) {\n case top:\n offsets = {\n x: commonX,\n y: reference.y - element.height\n };\n break;\n\n case bottom:\n offsets = {\n x: commonX,\n y: reference.y + reference.height\n };\n break;\n\n case right:\n offsets = {\n x: reference.x + reference.width,\n y: commonY\n };\n break;\n\n case left:\n offsets = {\n x: reference.x - element.width,\n y: commonY\n };\n break;\n\n default:\n offsets = {\n x: reference.x,\n y: reference.y\n };\n }\n\n var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null;\n\n if (mainAxis != null) {\n var len = mainAxis === 'y' ? 'height' : 'width';\n\n switch (variation) {\n case start:\n offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2);\n break;\n\n case end:\n offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2);\n break;\n\n default:\n }\n }\n\n return offsets;\n}","import getClippingRect from \"../dom-utils/getClippingRect.js\";\nimport getDocumentElement from \"../dom-utils/getDocumentElement.js\";\nimport getBoundingClientRect from \"../dom-utils/getBoundingClientRect.js\";\nimport computeOffsets from \"./computeOffsets.js\";\nimport rectToClientRect from \"./rectToClientRect.js\";\nimport { clippingParents, reference, popper, bottom, top, right, basePlacements, viewport } from \"../enums.js\";\nimport { isElement } from \"../dom-utils/instanceOf.js\";\nimport mergePaddingObject from \"./mergePaddingObject.js\";\nimport expandToHashMap from \"./expandToHashMap.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport default function detectOverflow(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n _options$placement = _options.placement,\n placement = _options$placement === void 0 ? state.placement : _options$placement,\n _options$strategy = _options.strategy,\n strategy = _options$strategy === void 0 ? state.strategy : _options$strategy,\n _options$boundary = _options.boundary,\n boundary = _options$boundary === void 0 ? clippingParents : _options$boundary,\n _options$rootBoundary = _options.rootBoundary,\n rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary,\n _options$elementConte = _options.elementContext,\n elementContext = _options$elementConte === void 0 ? popper : _options$elementConte,\n _options$altBoundary = _options.altBoundary,\n altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary,\n _options$padding = _options.padding,\n padding = _options$padding === void 0 ? 0 : _options$padding;\n var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements));\n var altContext = elementContext === popper ? reference : popper;\n var popperRect = state.rects.popper;\n var element = state.elements[altBoundary ? altContext : elementContext];\n var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy);\n var referenceClientRect = getBoundingClientRect(state.elements.reference);\n var popperOffsets = computeOffsets({\n reference: referenceClientRect,\n element: popperRect,\n strategy: 'absolute',\n placement: placement\n });\n var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets));\n var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect\n // 0 or negative = within the clipping rect\n\n var overflowOffsets = {\n top: clippingClientRect.top - elementClientRect.top + paddingObject.top,\n bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom,\n left: clippingClientRect.left - elementClientRect.left + paddingObject.left,\n right: elementClientRect.right - clippingClientRect.right + paddingObject.right\n };\n var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element\n\n if (elementContext === popper && offsetData) {\n var offset = offsetData[placement];\n Object.keys(overflowOffsets).forEach(function (key) {\n var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1;\n var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x';\n overflowOffsets[key] += offset[axis] * multiply;\n });\n }\n\n return overflowOffsets;\n}","import getOppositePlacement from \"../utils/getOppositePlacement.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getOppositeVariationPlacement from \"../utils/getOppositeVariationPlacement.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport computeAutoPlacement from \"../utils/computeAutoPlacement.js\";\nimport { bottom, top, start, right, left, auto } from \"../enums.js\";\nimport getVariation from \"../utils/getVariation.js\"; // eslint-disable-next-line import/no-unused-modules\n\nfunction getExpandedFallbackPlacements(placement) {\n if (getBasePlacement(placement) === auto) {\n return [];\n }\n\n var oppositePlacement = getOppositePlacement(placement);\n return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)];\n}\n\nfunction flip(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n\n if (state.modifiersData[name]._skip) {\n return;\n }\n\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis,\n specifiedFallbackPlacements = options.fallbackPlacements,\n padding = options.padding,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n _options$flipVariatio = options.flipVariations,\n flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio,\n allowedAutoPlacements = options.allowedAutoPlacements;\n var preferredPlacement = state.options.placement;\n var basePlacement = getBasePlacement(preferredPlacement);\n var isBasePlacement = basePlacement === preferredPlacement;\n var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement));\n var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) {\n return acc.concat(getBasePlacement(placement) === auto ? computeAutoPlacement(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n flipVariations: flipVariations,\n allowedAutoPlacements: allowedAutoPlacements\n }) : placement);\n }, []);\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var checksMap = new Map();\n var makeFallbackChecks = true;\n var firstFittingPlacement = placements[0];\n\n for (var i = 0; i < placements.length; i++) {\n var placement = placements[i];\n\n var _basePlacement = getBasePlacement(placement);\n\n var isStartVariation = getVariation(placement) === start;\n var isVertical = [top, bottom].indexOf(_basePlacement) >= 0;\n var len = isVertical ? 'width' : 'height';\n var overflow = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n altBoundary: altBoundary,\n padding: padding\n });\n var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top;\n\n if (referenceRect[len] > popperRect[len]) {\n mainVariationSide = getOppositePlacement(mainVariationSide);\n }\n\n var altVariationSide = getOppositePlacement(mainVariationSide);\n var checks = [];\n\n if (checkMainAxis) {\n checks.push(overflow[_basePlacement] <= 0);\n }\n\n if (checkAltAxis) {\n checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0);\n }\n\n if (checks.every(function (check) {\n return check;\n })) {\n firstFittingPlacement = placement;\n makeFallbackChecks = false;\n break;\n }\n\n checksMap.set(placement, checks);\n }\n\n if (makeFallbackChecks) {\n // `2` may be desired in some cases – research later\n var numberOfChecks = flipVariations ? 3 : 1;\n\n var _loop = function _loop(_i) {\n var fittingPlacement = placements.find(function (placement) {\n var checks = checksMap.get(placement);\n\n if (checks) {\n return checks.slice(0, _i).every(function (check) {\n return check;\n });\n }\n });\n\n if (fittingPlacement) {\n firstFittingPlacement = fittingPlacement;\n return \"break\";\n }\n };\n\n for (var _i = numberOfChecks; _i > 0; _i--) {\n var _ret = _loop(_i);\n\n if (_ret === \"break\") break;\n }\n }\n\n if (state.placement !== firstFittingPlacement) {\n state.modifiersData[name]._skip = true;\n state.placement = firstFittingPlacement;\n state.reset = true;\n }\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'flip',\n enabled: true,\n phase: 'main',\n fn: flip,\n requiresIfExists: ['offset'],\n data: {\n _skip: false\n }\n};","import getVariation from \"./getVariation.js\";\nimport { variationPlacements, basePlacements, placements as allPlacements } from \"../enums.js\";\nimport detectOverflow from \"./detectOverflow.js\";\nimport getBasePlacement from \"./getBasePlacement.js\";\nexport default function computeAutoPlacement(state, options) {\n if (options === void 0) {\n options = {};\n }\n\n var _options = options,\n placement = _options.placement,\n boundary = _options.boundary,\n rootBoundary = _options.rootBoundary,\n padding = _options.padding,\n flipVariations = _options.flipVariations,\n _options$allowedAutoP = _options.allowedAutoPlacements,\n allowedAutoPlacements = _options$allowedAutoP === void 0 ? allPlacements : _options$allowedAutoP;\n var variation = getVariation(placement);\n var placements = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) {\n return getVariation(placement) === variation;\n }) : basePlacements;\n var allowedPlacements = placements.filter(function (placement) {\n return allowedAutoPlacements.indexOf(placement) >= 0;\n });\n\n if (allowedPlacements.length === 0) {\n allowedPlacements = placements;\n } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions...\n\n\n var overflows = allowedPlacements.reduce(function (acc, placement) {\n acc[placement] = detectOverflow(state, {\n placement: placement,\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding\n })[getBasePlacement(placement)];\n return acc;\n }, {});\n return Object.keys(overflows).sort(function (a, b) {\n return overflows[a] - overflows[b];\n });\n}","import { top, bottom, left, right } from \"../enums.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\n\nfunction getSideOffsets(overflow, rect, preventedOffsets) {\n if (preventedOffsets === void 0) {\n preventedOffsets = {\n x: 0,\n y: 0\n };\n }\n\n return {\n top: overflow.top - rect.height - preventedOffsets.y,\n right: overflow.right - rect.width + preventedOffsets.x,\n bottom: overflow.bottom - rect.height + preventedOffsets.y,\n left: overflow.left - rect.width - preventedOffsets.x\n };\n}\n\nfunction isAnySideFullyClipped(overflow) {\n return [top, right, bottom, left].some(function (side) {\n return overflow[side] >= 0;\n });\n}\n\nfunction hide(_ref) {\n var state = _ref.state,\n name = _ref.name;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var preventedOffsets = state.modifiersData.preventOverflow;\n var referenceOverflow = detectOverflow(state, {\n elementContext: 'reference'\n });\n var popperAltOverflow = detectOverflow(state, {\n altBoundary: true\n });\n var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect);\n var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets);\n var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets);\n var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets);\n state.modifiersData[name] = {\n referenceClippingOffsets: referenceClippingOffsets,\n popperEscapeOffsets: popperEscapeOffsets,\n isReferenceHidden: isReferenceHidden,\n hasPopperEscaped: hasPopperEscaped\n };\n state.attributes.popper = Object.assign({}, state.attributes.popper, {\n 'data-popper-reference-hidden': isReferenceHidden,\n 'data-popper-escaped': hasPopperEscaped\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'hide',\n enabled: true,\n phase: 'main',\n requiresIfExists: ['preventOverflow'],\n fn: hide\n};","import getBasePlacement from \"../utils/getBasePlacement.js\";\nimport { top, left, right, placements } from \"../enums.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport function distanceAndSkiddingToXY(placement, rects, offset) {\n var basePlacement = getBasePlacement(placement);\n var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1;\n\n var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, {\n placement: placement\n })) : offset,\n skidding = _ref[0],\n distance = _ref[1];\n\n skidding = skidding || 0;\n distance = (distance || 0) * invertDistance;\n return [left, right].indexOf(basePlacement) >= 0 ? {\n x: distance,\n y: skidding\n } : {\n x: skidding,\n y: distance\n };\n}\n\nfunction offset(_ref2) {\n var state = _ref2.state,\n options = _ref2.options,\n name = _ref2.name;\n var _options$offset = options.offset,\n offset = _options$offset === void 0 ? [0, 0] : _options$offset;\n var data = placements.reduce(function (acc, placement) {\n acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset);\n return acc;\n }, {});\n var _data$state$placement = data[state.placement],\n x = _data$state$placement.x,\n y = _data$state$placement.y;\n\n if (state.modifiersData.popperOffsets != null) {\n state.modifiersData.popperOffsets.x += x;\n state.modifiersData.popperOffsets.y += y;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'offset',\n enabled: true,\n phase: 'main',\n requires: ['popperOffsets'],\n fn: offset\n};","import computeOffsets from \"../utils/computeOffsets.js\";\n\nfunction popperOffsets(_ref) {\n var state = _ref.state,\n name = _ref.name;\n // Offsets are the actual position the popper needs to have to be\n // properly positioned near its reference element\n // This is the most basic placement, and will be adjusted by\n // the modifiers in the next step\n state.modifiersData[name] = computeOffsets({\n reference: state.rects.reference,\n element: state.rects.popper,\n strategy: 'absolute',\n placement: state.placement\n });\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'popperOffsets',\n enabled: true,\n phase: 'read',\n fn: popperOffsets,\n data: {}\n};","import { top, left, right, bottom, start } from \"../enums.js\";\nimport getBasePlacement from \"../utils/getBasePlacement.js\";\nimport getMainAxisFromPlacement from \"../utils/getMainAxisFromPlacement.js\";\nimport getAltAxis from \"../utils/getAltAxis.js\";\nimport { within, withinMaxClamp } from \"../utils/within.js\";\nimport getLayoutRect from \"../dom-utils/getLayoutRect.js\";\nimport getOffsetParent from \"../dom-utils/getOffsetParent.js\";\nimport detectOverflow from \"../utils/detectOverflow.js\";\nimport getVariation from \"../utils/getVariation.js\";\nimport getFreshSideObject from \"../utils/getFreshSideObject.js\";\nimport { min as mathMin, max as mathMax } from \"../utils/math.js\";\n\nfunction preventOverflow(_ref) {\n var state = _ref.state,\n options = _ref.options,\n name = _ref.name;\n var _options$mainAxis = options.mainAxis,\n checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis,\n _options$altAxis = options.altAxis,\n checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis,\n boundary = options.boundary,\n rootBoundary = options.rootBoundary,\n altBoundary = options.altBoundary,\n padding = options.padding,\n _options$tether = options.tether,\n tether = _options$tether === void 0 ? true : _options$tether,\n _options$tetherOffset = options.tetherOffset,\n tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset;\n var overflow = detectOverflow(state, {\n boundary: boundary,\n rootBoundary: rootBoundary,\n padding: padding,\n altBoundary: altBoundary\n });\n var basePlacement = getBasePlacement(state.placement);\n var variation = getVariation(state.placement);\n var isBasePlacement = !variation;\n var mainAxis = getMainAxisFromPlacement(basePlacement);\n var altAxis = getAltAxis(mainAxis);\n var popperOffsets = state.modifiersData.popperOffsets;\n var referenceRect = state.rects.reference;\n var popperRect = state.rects.popper;\n var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, {\n placement: state.placement\n })) : tetherOffset;\n var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? {\n mainAxis: tetherOffsetValue,\n altAxis: tetherOffsetValue\n } : Object.assign({\n mainAxis: 0,\n altAxis: 0\n }, tetherOffsetValue);\n var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null;\n var data = {\n x: 0,\n y: 0\n };\n\n if (!popperOffsets) {\n return;\n }\n\n if (checkMainAxis) {\n var _offsetModifierState$;\n\n var mainSide = mainAxis === 'y' ? top : left;\n var altSide = mainAxis === 'y' ? bottom : right;\n var len = mainAxis === 'y' ? 'height' : 'width';\n var offset = popperOffsets[mainAxis];\n var min = offset + overflow[mainSide];\n var max = offset - overflow[altSide];\n var additive = tether ? -popperRect[len] / 2 : 0;\n var minLen = variation === start ? referenceRect[len] : popperRect[len];\n var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go\n // outside the reference bounds\n\n var arrowElement = state.elements.arrow;\n var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : {\n width: 0,\n height: 0\n };\n var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject();\n var arrowPaddingMin = arrowPaddingObject[mainSide];\n var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want\n // to include its full size in the calculation. If the reference is small\n // and near the edge of a boundary, the popper can overflow even if the\n // reference is not overflowing as well (e.g. virtual elements with no\n // width or height)\n\n var arrowLen = within(0, referenceRect[len], arrowRect[len]);\n var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis;\n var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis;\n var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow);\n var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0;\n var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0;\n var tetherMin = offset + minOffset - offsetModifierValue - clientOffset;\n var tetherMax = offset + maxOffset - offsetModifierValue;\n var preventedOffset = within(tether ? mathMin(min, tetherMin) : min, offset, tether ? mathMax(max, tetherMax) : max);\n popperOffsets[mainAxis] = preventedOffset;\n data[mainAxis] = preventedOffset - offset;\n }\n\n if (checkAltAxis) {\n var _offsetModifierState$2;\n\n var _mainSide = mainAxis === 'x' ? top : left;\n\n var _altSide = mainAxis === 'x' ? bottom : right;\n\n var _offset = popperOffsets[altAxis];\n\n var _len = altAxis === 'y' ? 'height' : 'width';\n\n var _min = _offset + overflow[_mainSide];\n\n var _max = _offset - overflow[_altSide];\n\n var isOriginSide = [top, left].indexOf(basePlacement) !== -1;\n\n var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0;\n\n var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis;\n\n var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max;\n\n var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max);\n\n popperOffsets[altAxis] = _preventedOffset;\n data[altAxis] = _preventedOffset - _offset;\n }\n\n state.modifiersData[name] = data;\n} // eslint-disable-next-line import/no-unused-modules\n\n\nexport default {\n name: 'preventOverflow',\n enabled: true,\n phase: 'main',\n fn: preventOverflow,\n requiresIfExists: ['offset']\n};","export default function getAltAxis(axis) {\n return axis === 'x' ? 'y' : 'x';\n}","import getBoundingClientRect from \"./getBoundingClientRect.js\";\nimport getNodeScroll from \"./getNodeScroll.js\";\nimport getNodeName from \"./getNodeName.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getWindowScrollBarX from \"./getWindowScrollBarX.js\";\nimport getDocumentElement from \"./getDocumentElement.js\";\nimport isScrollParent from \"./isScrollParent.js\";\nimport { round } from \"../utils/math.js\";\n\nfunction isElementScaled(element) {\n var rect = element.getBoundingClientRect();\n var scaleX = round(rect.width) / element.offsetWidth || 1;\n var scaleY = round(rect.height) / element.offsetHeight || 1;\n return scaleX !== 1 || scaleY !== 1;\n} // Returns the composite rect of an element relative to its offsetParent.\n// Composite means it takes into account transforms as well as layout.\n\n\nexport default function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) {\n if (isFixed === void 0) {\n isFixed = false;\n }\n\n var isOffsetParentAnElement = isHTMLElement(offsetParent);\n var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent);\n var documentElement = getDocumentElement(offsetParent);\n var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed);\n var scroll = {\n scrollLeft: 0,\n scrollTop: 0\n };\n var offsets = {\n x: 0,\n y: 0\n };\n\n if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) {\n if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078\n isScrollParent(documentElement)) {\n scroll = getNodeScroll(offsetParent);\n }\n\n if (isHTMLElement(offsetParent)) {\n offsets = getBoundingClientRect(offsetParent, true);\n offsets.x += offsetParent.clientLeft;\n offsets.y += offsetParent.clientTop;\n } else if (documentElement) {\n offsets.x = getWindowScrollBarX(documentElement);\n }\n }\n\n return {\n x: rect.left + scroll.scrollLeft - offsets.x,\n y: rect.top + scroll.scrollTop - offsets.y,\n width: rect.width,\n height: rect.height\n };\n}","import getWindowScroll from \"./getWindowScroll.js\";\nimport getWindow from \"./getWindow.js\";\nimport { isHTMLElement } from \"./instanceOf.js\";\nimport getHTMLElementScroll from \"./getHTMLElementScroll.js\";\nexport default function getNodeScroll(node) {\n if (node === getWindow(node) || !isHTMLElement(node)) {\n return getWindowScroll(node);\n } else {\n return getHTMLElementScroll(node);\n }\n}","export default function getHTMLElementScroll(element) {\n return {\n scrollLeft: element.scrollLeft,\n scrollTop: element.scrollTop\n };\n}","import { modifierPhases } from \"../enums.js\"; // source: https://stackoverflow.com/questions/49875255\n\nfunction order(modifiers) {\n var map = new Map();\n var visited = new Set();\n var result = [];\n modifiers.forEach(function (modifier) {\n map.set(modifier.name, modifier);\n }); // On visiting object, check for its dependencies and visit them recursively\n\n function sort(modifier) {\n visited.add(modifier.name);\n var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []);\n requires.forEach(function (dep) {\n if (!visited.has(dep)) {\n var depModifier = map.get(dep);\n\n if (depModifier) {\n sort(depModifier);\n }\n }\n });\n result.push(modifier);\n }\n\n modifiers.forEach(function (modifier) {\n if (!visited.has(modifier.name)) {\n // check for visited object\n sort(modifier);\n }\n });\n return result;\n}\n\nexport default function orderModifiers(modifiers) {\n // order based on dependencies\n var orderedModifiers = order(modifiers); // order based on phase\n\n return modifierPhases.reduce(function (acc, phase) {\n return acc.concat(orderedModifiers.filter(function (modifier) {\n return modifier.phase === phase;\n }));\n }, []);\n}","import getCompositeRect from \"./dom-utils/getCompositeRect.js\";\nimport getLayoutRect from \"./dom-utils/getLayoutRect.js\";\nimport listScrollParents from \"./dom-utils/listScrollParents.js\";\nimport getOffsetParent from \"./dom-utils/getOffsetParent.js\";\nimport orderModifiers from \"./utils/orderModifiers.js\";\nimport debounce from \"./utils/debounce.js\";\nimport mergeByName from \"./utils/mergeByName.js\";\nimport detectOverflow from \"./utils/detectOverflow.js\";\nimport { isElement } from \"./dom-utils/instanceOf.js\";\nvar DEFAULT_OPTIONS = {\n placement: 'bottom',\n modifiers: [],\n strategy: 'absolute'\n};\n\nfunction areValidElements() {\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n return !args.some(function (element) {\n return !(element && typeof element.getBoundingClientRect === 'function');\n });\n}\n\nexport function popperGenerator(generatorOptions) {\n if (generatorOptions === void 0) {\n generatorOptions = {};\n }\n\n var _generatorOptions = generatorOptions,\n _generatorOptions$def = _generatorOptions.defaultModifiers,\n defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def,\n _generatorOptions$def2 = _generatorOptions.defaultOptions,\n defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2;\n return function createPopper(reference, popper, options) {\n if (options === void 0) {\n options = defaultOptions;\n }\n\n var state = {\n placement: 'bottom',\n orderedModifiers: [],\n options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions),\n modifiersData: {},\n elements: {\n reference: reference,\n popper: popper\n },\n attributes: {},\n styles: {}\n };\n var effectCleanupFns = [];\n var isDestroyed = false;\n var instance = {\n state: state,\n setOptions: function setOptions(setOptionsAction) {\n var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction;\n cleanupModifierEffects();\n state.options = Object.assign({}, defaultOptions, state.options, options);\n state.scrollParents = {\n reference: isElement(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [],\n popper: listScrollParents(popper)\n }; // Orders the modifiers based on their dependencies and `phase`\n // properties\n\n var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers\n\n state.orderedModifiers = orderedModifiers.filter(function (m) {\n return m.enabled;\n });\n runModifierEffects();\n return instance.update();\n },\n // Sync update – it will always be executed, even if not necessary. This\n // is useful for low frequency updates where sync behavior simplifies the\n // logic.\n // For high frequency updates (e.g. `resize` and `scroll` events), always\n // prefer the async Popper#update method\n forceUpdate: function forceUpdate() {\n if (isDestroyed) {\n return;\n }\n\n var _state$elements = state.elements,\n reference = _state$elements.reference,\n popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements\n // anymore\n\n if (!areValidElements(reference, popper)) {\n return;\n } // Store the reference and popper rects to be read by modifiers\n\n\n state.rects = {\n reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'),\n popper: getLayoutRect(popper)\n }; // Modifiers have the ability to reset the current update cycle. The\n // most common use case for this is the `flip` modifier changing the\n // placement, which then needs to re-run all the modifiers, because the\n // logic was previously ran for the previous placement and is therefore\n // stale/incorrect\n\n state.reset = false;\n state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier\n // is filled with the initial data specified by the modifier. This means\n // it doesn't persist and is fresh on each update.\n // To ensure persistent data, use `${name}#persistent`\n\n state.orderedModifiers.forEach(function (modifier) {\n return state.modifiersData[modifier.name] = Object.assign({}, modifier.data);\n });\n\n for (var index = 0; index < state.orderedModifiers.length; index++) {\n if (state.reset === true) {\n state.reset = false;\n index = -1;\n continue;\n }\n\n var _state$orderedModifie = state.orderedModifiers[index],\n fn = _state$orderedModifie.fn,\n _state$orderedModifie2 = _state$orderedModifie.options,\n _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2,\n name = _state$orderedModifie.name;\n\n if (typeof fn === 'function') {\n state = fn({\n state: state,\n options: _options,\n name: name,\n instance: instance\n }) || state;\n }\n }\n },\n // Async and optimistically optimized update – it will not be executed if\n // not necessary (debounced to run at most once-per-tick)\n update: debounce(function () {\n return new Promise(function (resolve) {\n instance.forceUpdate();\n resolve(state);\n });\n }),\n destroy: function destroy() {\n cleanupModifierEffects();\n isDestroyed = true;\n }\n };\n\n if (!areValidElements(reference, popper)) {\n return instance;\n }\n\n instance.setOptions(options).then(function (state) {\n if (!isDestroyed && options.onFirstUpdate) {\n options.onFirstUpdate(state);\n }\n }); // Modifiers have the ability to execute arbitrary code before the first\n // update cycle runs. They will be executed in the same order as the update\n // cycle. This is useful when a modifier adds some persistent data that\n // other modifiers need to use, but the modifier is run after the dependent\n // one.\n\n function runModifierEffects() {\n state.orderedModifiers.forEach(function (_ref) {\n var name = _ref.name,\n _ref$options = _ref.options,\n options = _ref$options === void 0 ? {} : _ref$options,\n effect = _ref.effect;\n\n if (typeof effect === 'function') {\n var cleanupFn = effect({\n state: state,\n name: name,\n instance: instance,\n options: options\n });\n\n var noopFn = function noopFn() {};\n\n effectCleanupFns.push(cleanupFn || noopFn);\n }\n });\n }\n\n function cleanupModifierEffects() {\n effectCleanupFns.forEach(function (fn) {\n return fn();\n });\n effectCleanupFns = [];\n }\n\n return instance;\n };\n}\nexport var createPopper = /*#__PURE__*/popperGenerator(); // eslint-disable-next-line import/no-unused-modules\n\nexport { detectOverflow };","export default function debounce(fn) {\n var pending;\n return function () {\n if (!pending) {\n pending = new Promise(function (resolve) {\n Promise.resolve().then(function () {\n pending = undefined;\n resolve(fn());\n });\n });\n }\n\n return pending;\n };\n}","export default function mergeByName(modifiers) {\n var merged = modifiers.reduce(function (merged, current) {\n var existing = merged[current.name];\n merged[current.name] = existing ? Object.assign({}, existing, current, {\n options: Object.assign({}, existing.options, current.options),\n data: Object.assign({}, existing.data, current.data)\n }) : current;\n return merged;\n }, {}); // IE11 does not support Object.values\n\n return Object.keys(merged).map(function (key) {\n return merged[key];\n });\n}","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nimport offset from \"./modifiers/offset.js\";\nimport flip from \"./modifiers/flip.js\";\nimport preventOverflow from \"./modifiers/preventOverflow.js\";\nimport arrow from \"./modifiers/arrow.js\";\nimport hide from \"./modifiers/hide.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles, offset, flip, preventOverflow, arrow, hide];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow }; // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper as createPopperLite } from \"./popper-lite.js\"; // eslint-disable-next-line import/no-unused-modules\n\nexport * from \"./modifiers/index.js\";","import { popperGenerator, detectOverflow } from \"./createPopper.js\";\nimport eventListeners from \"./modifiers/eventListeners.js\";\nimport popperOffsets from \"./modifiers/popperOffsets.js\";\nimport computeStyles from \"./modifiers/computeStyles.js\";\nimport applyStyles from \"./modifiers/applyStyles.js\";\nvar defaultModifiers = [eventListeners, popperOffsets, computeStyles, applyStyles];\nvar createPopper = /*#__PURE__*/popperGenerator({\n defaultModifiers: defaultModifiers\n}); // eslint-disable-next-line import/no-unused-modules\n\nexport { createPopper, popperGenerator, defaultModifiers, detectOverflow };","/*!\n * Bootstrap v5.3.2 (https://getbootstrap.com/)\n * Copyright 2011-2023 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n */\nimport * as Popper from '@popperjs/core';\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/data.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n/**\n * Constants\n */\n\nconst elementMap = new Map();\nconst Data = {\n set(element, key, instance) {\n if (!elementMap.has(element)) {\n elementMap.set(element, new Map());\n }\n const instanceMap = elementMap.get(element);\n\n // make it clear we only want one instance per element\n // can be removed later when multiple key/instances are fine to be used\n if (!instanceMap.has(key) && instanceMap.size !== 0) {\n // eslint-disable-next-line no-console\n console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(instanceMap.keys())[0]}.`);\n return;\n }\n instanceMap.set(key, instance);\n },\n get(element, key) {\n if (elementMap.has(element)) {\n return elementMap.get(element).get(key) || null;\n }\n return null;\n },\n remove(element, key) {\n if (!elementMap.has(element)) {\n return;\n }\n const instanceMap = elementMap.get(element);\n instanceMap.delete(key);\n\n // free up element references if there are no instances left for an element\n if (instanceMap.size === 0) {\n elementMap.delete(element);\n }\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/index.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst MAX_UID = 1000000;\nconst MILLISECONDS_MULTIPLIER = 1000;\nconst TRANSITION_END = 'transitionend';\n\n/**\n * Properly escape IDs selectors to handle weird IDs\n * @param {string} selector\n * @returns {string}\n */\nconst parseSelector = selector => {\n if (selector && window.CSS && window.CSS.escape) {\n // document.querySelector needs escaping to handle IDs (html5+) containing for instance /\n selector = selector.replace(/#([^\\s\"#']+)/g, (match, id) => `#${CSS.escape(id)}`);\n }\n return selector;\n};\n\n// Shout-out Angus Croll (https://goo.gl/pxwQGp)\nconst toType = object => {\n if (object === null || object === undefined) {\n return `${object}`;\n }\n return Object.prototype.toString.call(object).match(/\\s([a-z]+)/i)[1].toLowerCase();\n};\n\n/**\n * Public Util API\n */\n\nconst getUID = prefix => {\n do {\n prefix += Math.floor(Math.random() * MAX_UID);\n } while (document.getElementById(prefix));\n return prefix;\n};\nconst getTransitionDurationFromElement = element => {\n if (!element) {\n return 0;\n }\n\n // Get transition-duration of the element\n let {\n transitionDuration,\n transitionDelay\n } = window.getComputedStyle(element);\n const floatTransitionDuration = Number.parseFloat(transitionDuration);\n const floatTransitionDelay = Number.parseFloat(transitionDelay);\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0;\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0];\n transitionDelay = transitionDelay.split(',')[0];\n return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER;\n};\nconst triggerTransitionEnd = element => {\n element.dispatchEvent(new Event(TRANSITION_END));\n};\nconst isElement = object => {\n if (!object || typeof object !== 'object') {\n return false;\n }\n if (typeof object.jquery !== 'undefined') {\n object = object[0];\n }\n return typeof object.nodeType !== 'undefined';\n};\nconst getElement = object => {\n // it's a jQuery object or a node element\n if (isElement(object)) {\n return object.jquery ? object[0] : object;\n }\n if (typeof object === 'string' && object.length > 0) {\n return document.querySelector(parseSelector(object));\n }\n return null;\n};\nconst isVisible = element => {\n if (!isElement(element) || element.getClientRects().length === 0) {\n return false;\n }\n const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible';\n // Handle `details` element as its content may falsie appear visible when it is closed\n const closedDetails = element.closest('details:not([open])');\n if (!closedDetails) {\n return elementIsVisible;\n }\n if (closedDetails !== element) {\n const summary = element.closest('summary');\n if (summary && summary.parentNode !== closedDetails) {\n return false;\n }\n if (summary === null) {\n return false;\n }\n }\n return elementIsVisible;\n};\nconst isDisabled = element => {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return true;\n }\n if (element.classList.contains('disabled')) {\n return true;\n }\n if (typeof element.disabled !== 'undefined') {\n return element.disabled;\n }\n return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false';\n};\nconst findShadowRoot = element => {\n if (!document.documentElement.attachShadow) {\n return null;\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode();\n return root instanceof ShadowRoot ? root : null;\n }\n if (element instanceof ShadowRoot) {\n return element;\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null;\n }\n return findShadowRoot(element.parentNode);\n};\nconst noop = () => {};\n\n/**\n * Trick to restart an element's animation\n *\n * @param {HTMLElement} element\n * @return void\n *\n * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation\n */\nconst reflow = element => {\n element.offsetHeight; // eslint-disable-line no-unused-expressions\n};\n\nconst getjQuery = () => {\n if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {\n return window.jQuery;\n }\n return null;\n};\nconst DOMContentLoadedCallbacks = [];\nconst onDOMContentLoaded = callback => {\n if (document.readyState === 'loading') {\n // add listener on the first call when the document is in loading state\n if (!DOMContentLoadedCallbacks.length) {\n document.addEventListener('DOMContentLoaded', () => {\n for (const callback of DOMContentLoadedCallbacks) {\n callback();\n }\n });\n }\n DOMContentLoadedCallbacks.push(callback);\n } else {\n callback();\n }\n};\nconst isRTL = () => document.documentElement.dir === 'rtl';\nconst defineJQueryPlugin = plugin => {\n onDOMContentLoaded(() => {\n const $ = getjQuery();\n /* istanbul ignore if */\n if ($) {\n const name = plugin.NAME;\n const JQUERY_NO_CONFLICT = $.fn[name];\n $.fn[name] = plugin.jQueryInterface;\n $.fn[name].Constructor = plugin;\n $.fn[name].noConflict = () => {\n $.fn[name] = JQUERY_NO_CONFLICT;\n return plugin.jQueryInterface;\n };\n }\n });\n};\nconst execute = (possibleCallback, args = [], defaultValue = possibleCallback) => {\n return typeof possibleCallback === 'function' ? possibleCallback(...args) : defaultValue;\n};\nconst executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {\n if (!waitForTransition) {\n execute(callback);\n return;\n }\n const durationPadding = 5;\n const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding;\n let called = false;\n const handler = ({\n target\n }) => {\n if (target !== transitionElement) {\n return;\n }\n called = true;\n transitionElement.removeEventListener(TRANSITION_END, handler);\n execute(callback);\n };\n transitionElement.addEventListener(TRANSITION_END, handler);\n setTimeout(() => {\n if (!called) {\n triggerTransitionEnd(transitionElement);\n }\n }, emulatedDuration);\n};\n\n/**\n * Return the previous/next element of a list.\n *\n * @param {array} list The list of elements\n * @param activeElement The active element\n * @param shouldGetNext Choose to get next or previous element\n * @param isCycleAllowed\n * @return {Element|elem} The proper element\n */\nconst getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {\n const listLength = list.length;\n let index = list.indexOf(activeElement);\n\n // if the element does not exist in the list return an element\n // depending on the direction and if cycle is allowed\n if (index === -1) {\n return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0];\n }\n index += shouldGetNext ? 1 : -1;\n if (isCycleAllowed) {\n index = (index + listLength) % listLength;\n }\n return list[Math.max(0, Math.min(index, listLength - 1))];\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/event-handler.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst namespaceRegex = /[^.]*(?=\\..*)\\.|.*/;\nconst stripNameRegex = /\\..*/;\nconst stripUidRegex = /::\\d+$/;\nconst eventRegistry = {}; // Events storage\nlet uidEvent = 1;\nconst customEvents = {\n mouseenter: 'mouseover',\n mouseleave: 'mouseout'\n};\nconst nativeEvents = new Set(['click', 'dblclick', 'mouseup', 'mousedown', 'contextmenu', 'mousewheel', 'DOMMouseScroll', 'mouseover', 'mouseout', 'mousemove', 'selectstart', 'selectend', 'keydown', 'keypress', 'keyup', 'orientationchange', 'touchstart', 'touchmove', 'touchend', 'touchcancel', 'pointerdown', 'pointermove', 'pointerup', 'pointerleave', 'pointercancel', 'gesturestart', 'gesturechange', 'gestureend', 'focus', 'blur', 'change', 'reset', 'select', 'submit', 'focusin', 'focusout', 'load', 'unload', 'beforeunload', 'resize', 'move', 'DOMContentLoaded', 'readystatechange', 'error', 'abort', 'scroll']);\n\n/**\n * Private methods\n */\n\nfunction makeEventUid(element, uid) {\n return uid && `${uid}::${uidEvent++}` || element.uidEvent || uidEvent++;\n}\nfunction getElementEvents(element) {\n const uid = makeEventUid(element);\n element.uidEvent = uid;\n eventRegistry[uid] = eventRegistry[uid] || {};\n return eventRegistry[uid];\n}\nfunction bootstrapHandler(element, fn) {\n return function handler(event) {\n hydrateObj(event, {\n delegateTarget: element\n });\n if (handler.oneOff) {\n EventHandler.off(element, event.type, fn);\n }\n return fn.apply(element, [event]);\n };\n}\nfunction bootstrapDelegationHandler(element, selector, fn) {\n return function handler(event) {\n const domElements = element.querySelectorAll(selector);\n for (let {\n target\n } = event; target && target !== this; target = target.parentNode) {\n for (const domElement of domElements) {\n if (domElement !== target) {\n continue;\n }\n hydrateObj(event, {\n delegateTarget: target\n });\n if (handler.oneOff) {\n EventHandler.off(element, event.type, selector, fn);\n }\n return fn.apply(target, [event]);\n }\n }\n };\n}\nfunction findHandler(events, callable, delegationSelector = null) {\n return Object.values(events).find(event => event.callable === callable && event.delegationSelector === delegationSelector);\n}\nfunction normalizeParameters(originalTypeEvent, handler, delegationFunction) {\n const isDelegated = typeof handler === 'string';\n // TODO: tooltip passes `false` instead of selector, so we need to check\n const callable = isDelegated ? delegationFunction : handler || delegationFunction;\n let typeEvent = getTypeEvent(originalTypeEvent);\n if (!nativeEvents.has(typeEvent)) {\n typeEvent = originalTypeEvent;\n }\n return [isDelegated, callable, typeEvent];\n}\nfunction addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n\n // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position\n // this prevents the handler from being dispatched the same way as mouseover or mouseout does\n if (originalTypeEvent in customEvents) {\n const wrapFunction = fn => {\n return function (event) {\n if (!event.relatedTarget || event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget)) {\n return fn.call(this, event);\n }\n };\n };\n callable = wrapFunction(callable);\n }\n const events = getElementEvents(element);\n const handlers = events[typeEvent] || (events[typeEvent] = {});\n const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null);\n if (previousFunction) {\n previousFunction.oneOff = previousFunction.oneOff && oneOff;\n return;\n }\n const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''));\n const fn = isDelegated ? bootstrapDelegationHandler(element, handler, callable) : bootstrapHandler(element, callable);\n fn.delegationSelector = isDelegated ? handler : null;\n fn.callable = callable;\n fn.oneOff = oneOff;\n fn.uidEvent = uid;\n handlers[uid] = fn;\n element.addEventListener(typeEvent, fn, isDelegated);\n}\nfunction removeHandler(element, events, typeEvent, handler, delegationSelector) {\n const fn = findHandler(events[typeEvent], handler, delegationSelector);\n if (!fn) {\n return;\n }\n element.removeEventListener(typeEvent, fn, Boolean(delegationSelector));\n delete events[typeEvent][fn.uidEvent];\n}\nfunction removeNamespacedHandlers(element, events, typeEvent, namespace) {\n const storeElementEvent = events[typeEvent] || {};\n for (const [handlerKey, event] of Object.entries(storeElementEvent)) {\n if (handlerKey.includes(namespace)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n}\nfunction getTypeEvent(event) {\n // allow to get the native events from namespaced events ('click.bs.button' --> 'click')\n event = event.replace(stripNameRegex, '');\n return customEvents[event] || event;\n}\nconst EventHandler = {\n on(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, false);\n },\n one(element, event, handler, delegationFunction) {\n addHandler(element, event, handler, delegationFunction, true);\n },\n off(element, originalTypeEvent, handler, delegationFunction) {\n if (typeof originalTypeEvent !== 'string' || !element) {\n return;\n }\n const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction);\n const inNamespace = typeEvent !== originalTypeEvent;\n const events = getElementEvents(element);\n const storeElementEvent = events[typeEvent] || {};\n const isNamespace = originalTypeEvent.startsWith('.');\n if (typeof callable !== 'undefined') {\n // Simplest case: handler is passed, remove that listener ONLY.\n if (!Object.keys(storeElementEvent).length) {\n return;\n }\n removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null);\n return;\n }\n if (isNamespace) {\n for (const elementEvent of Object.keys(events)) {\n removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1));\n }\n }\n for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {\n const handlerKey = keyHandlers.replace(stripUidRegex, '');\n if (!inNamespace || originalTypeEvent.includes(handlerKey)) {\n removeHandler(element, events, typeEvent, event.callable, event.delegationSelector);\n }\n }\n },\n trigger(element, event, args) {\n if (typeof event !== 'string' || !element) {\n return null;\n }\n const $ = getjQuery();\n const typeEvent = getTypeEvent(event);\n const inNamespace = event !== typeEvent;\n let jQueryEvent = null;\n let bubbles = true;\n let nativeDispatch = true;\n let defaultPrevented = false;\n if (inNamespace && $) {\n jQueryEvent = $.Event(event, args);\n $(element).trigger(jQueryEvent);\n bubbles = !jQueryEvent.isPropagationStopped();\n nativeDispatch = !jQueryEvent.isImmediatePropagationStopped();\n defaultPrevented = jQueryEvent.isDefaultPrevented();\n }\n const evt = hydrateObj(new Event(event, {\n bubbles,\n cancelable: true\n }), args);\n if (defaultPrevented) {\n evt.preventDefault();\n }\n if (nativeDispatch) {\n element.dispatchEvent(evt);\n }\n if (evt.defaultPrevented && jQueryEvent) {\n jQueryEvent.preventDefault();\n }\n return evt;\n }\n};\nfunction hydrateObj(obj, meta = {}) {\n for (const [key, value] of Object.entries(meta)) {\n try {\n obj[key] = value;\n } catch (_unused) {\n Object.defineProperty(obj, key, {\n configurable: true,\n get() {\n return value;\n }\n });\n }\n }\n return obj;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/manipulator.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nfunction normalizeData(value) {\n if (value === 'true') {\n return true;\n }\n if (value === 'false') {\n return false;\n }\n if (value === Number(value).toString()) {\n return Number(value);\n }\n if (value === '' || value === 'null') {\n return null;\n }\n if (typeof value !== 'string') {\n return value;\n }\n try {\n return JSON.parse(decodeURIComponent(value));\n } catch (_unused) {\n return value;\n }\n}\nfunction normalizeDataKey(key) {\n return key.replace(/[A-Z]/g, chr => `-${chr.toLowerCase()}`);\n}\nconst Manipulator = {\n setDataAttribute(element, key, value) {\n element.setAttribute(`data-bs-${normalizeDataKey(key)}`, value);\n },\n removeDataAttribute(element, key) {\n element.removeAttribute(`data-bs-${normalizeDataKey(key)}`);\n },\n getDataAttributes(element) {\n if (!element) {\n return {};\n }\n const attributes = {};\n const bsKeys = Object.keys(element.dataset).filter(key => key.startsWith('bs') && !key.startsWith('bsConfig'));\n for (const key of bsKeys) {\n let pureKey = key.replace(/^bs/, '');\n pureKey = pureKey.charAt(0).toLowerCase() + pureKey.slice(1, pureKey.length);\n attributes[pureKey] = normalizeData(element.dataset[key]);\n }\n return attributes;\n },\n getDataAttribute(element, key) {\n return normalizeData(element.getAttribute(`data-bs-${normalizeDataKey(key)}`));\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/config.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Class definition\n */\n\nclass Config {\n // Getters\n static get Default() {\n return {};\n }\n static get DefaultType() {\n return {};\n }\n static get NAME() {\n throw new Error('You have to implement the static method \"NAME\", for each component!');\n }\n _getConfig(config) {\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n _configAfterMerge(config) {\n return config;\n }\n _mergeConfigObj(config, element) {\n const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {}; // try to parse\n\n return {\n ...this.constructor.Default,\n ...(typeof jsonConfig === 'object' ? jsonConfig : {}),\n ...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),\n ...(typeof config === 'object' ? config : {})\n };\n }\n _typeCheckConfig(config, configTypes = this.constructor.DefaultType) {\n for (const [property, expectedTypes] of Object.entries(configTypes)) {\n const value = config[property];\n const valueType = isElement(value) ? 'element' : toType(value);\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option \"${property}\" provided type \"${valueType}\" but expected type \"${expectedTypes}\".`);\n }\n }\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap base-component.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst VERSION = '5.3.2';\n\n/**\n * Class definition\n */\n\nclass BaseComponent extends Config {\n constructor(element, config) {\n super();\n element = getElement(element);\n if (!element) {\n return;\n }\n this._element = element;\n this._config = this._getConfig(config);\n Data.set(this._element, this.constructor.DATA_KEY, this);\n }\n\n // Public\n dispose() {\n Data.remove(this._element, this.constructor.DATA_KEY);\n EventHandler.off(this._element, this.constructor.EVENT_KEY);\n for (const propertyName of Object.getOwnPropertyNames(this)) {\n this[propertyName] = null;\n }\n }\n _queueCallback(callback, element, isAnimated = true) {\n executeAfterTransition(callback, element, isAnimated);\n }\n _getConfig(config) {\n config = this._mergeConfigObj(config, this._element);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n\n // Static\n static getInstance(element) {\n return Data.get(getElement(element), this.DATA_KEY);\n }\n static getOrCreateInstance(element, config = {}) {\n return this.getInstance(element) || new this(element, typeof config === 'object' ? config : null);\n }\n static get VERSION() {\n return VERSION;\n }\n static get DATA_KEY() {\n return `bs.${this.NAME}`;\n }\n static get EVENT_KEY() {\n return `.${this.DATA_KEY}`;\n }\n static eventName(name) {\n return `${name}${this.EVENT_KEY}`;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dom/selector-engine.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst getSelector = element => {\n let selector = element.getAttribute('data-bs-target');\n if (!selector || selector === '#') {\n let hrefAttribute = element.getAttribute('href');\n\n // The only valid content that could double as a selector are IDs or classes,\n // so everything starting with `#` or `.`. If a \"real\" URL is used as the selector,\n // `document.querySelector` will rightfully complain it is invalid.\n // See https://github.com/twbs/bootstrap/issues/32273\n if (!hrefAttribute || !hrefAttribute.includes('#') && !hrefAttribute.startsWith('.')) {\n return null;\n }\n\n // Just in case some CMS puts out a full URL with the anchor appended\n if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {\n hrefAttribute = `#${hrefAttribute.split('#')[1]}`;\n }\n selector = hrefAttribute && hrefAttribute !== '#' ? parseSelector(hrefAttribute.trim()) : null;\n }\n return selector;\n};\nconst SelectorEngine = {\n find(selector, element = document.documentElement) {\n return [].concat(...Element.prototype.querySelectorAll.call(element, selector));\n },\n findOne(selector, element = document.documentElement) {\n return Element.prototype.querySelector.call(element, selector);\n },\n children(element, selector) {\n return [].concat(...element.children).filter(child => child.matches(selector));\n },\n parents(element, selector) {\n const parents = [];\n let ancestor = element.parentNode.closest(selector);\n while (ancestor) {\n parents.push(ancestor);\n ancestor = ancestor.parentNode.closest(selector);\n }\n return parents;\n },\n prev(element, selector) {\n let previous = element.previousElementSibling;\n while (previous) {\n if (previous.matches(selector)) {\n return [previous];\n }\n previous = previous.previousElementSibling;\n }\n return [];\n },\n // TODO: this is now unused; remove later along with prev()\n next(element, selector) {\n let next = element.nextElementSibling;\n while (next) {\n if (next.matches(selector)) {\n return [next];\n }\n next = next.nextElementSibling;\n }\n return [];\n },\n focusableChildren(element) {\n const focusables = ['a', 'button', 'input', 'textarea', 'select', 'details', '[tabindex]', '[contenteditable=\"true\"]'].map(selector => `${selector}:not([tabindex^=\"-\"])`).join(',');\n return this.find(focusables, element).filter(el => !isDisabled(el) && isVisible(el));\n },\n getSelectorFromElement(element) {\n const selector = getSelector(element);\n if (selector) {\n return SelectorEngine.findOne(selector) ? selector : null;\n }\n return null;\n },\n getElementFromSelector(element) {\n const selector = getSelector(element);\n return selector ? SelectorEngine.findOne(selector) : null;\n },\n getMultipleElementsFromSelector(element) {\n const selector = getSelector(element);\n return selector ? SelectorEngine.find(selector) : [];\n }\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/component-functions.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nconst enableDismissTrigger = (component, method = 'hide') => {\n const clickEvent = `click.dismiss${component.EVENT_KEY}`;\n const name = component.NAME;\n EventHandler.on(document, clickEvent, `[data-bs-dismiss=\"${name}\"]`, function (event) {\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n if (isDisabled(this)) {\n return;\n }\n const target = SelectorEngine.getElementFromSelector(this) || this.closest(`.${name}`);\n const instance = component.getOrCreateInstance(target);\n\n // Method argument is left, for Alert and only, as it doesn't implement the 'hide' method\n instance[method]();\n });\n};\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$f = 'alert';\nconst DATA_KEY$a = 'bs.alert';\nconst EVENT_KEY$b = `.${DATA_KEY$a}`;\nconst EVENT_CLOSE = `close${EVENT_KEY$b}`;\nconst EVENT_CLOSED = `closed${EVENT_KEY$b}`;\nconst CLASS_NAME_FADE$5 = 'fade';\nconst CLASS_NAME_SHOW$8 = 'show';\n\n/**\n * Class definition\n */\n\nclass Alert extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$f;\n }\n\n // Public\n close() {\n const closeEvent = EventHandler.trigger(this._element, EVENT_CLOSE);\n if (closeEvent.defaultPrevented) {\n return;\n }\n this._element.classList.remove(CLASS_NAME_SHOW$8);\n const isAnimated = this._element.classList.contains(CLASS_NAME_FADE$5);\n this._queueCallback(() => this._destroyElement(), this._element, isAnimated);\n }\n\n // Private\n _destroyElement() {\n this._element.remove();\n EventHandler.trigger(this._element, EVENT_CLOSED);\n this.dispose();\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Alert.getOrCreateInstance(this);\n if (typeof config !== 'string') {\n return;\n }\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](this);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nenableDismissTrigger(Alert, 'close');\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Alert);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$e = 'button';\nconst DATA_KEY$9 = 'bs.button';\nconst EVENT_KEY$a = `.${DATA_KEY$9}`;\nconst DATA_API_KEY$6 = '.data-api';\nconst CLASS_NAME_ACTIVE$3 = 'active';\nconst SELECTOR_DATA_TOGGLE$5 = '[data-bs-toggle=\"button\"]';\nconst EVENT_CLICK_DATA_API$6 = `click${EVENT_KEY$a}${DATA_API_KEY$6}`;\n\n/**\n * Class definition\n */\n\nclass Button extends BaseComponent {\n // Getters\n static get NAME() {\n return NAME$e;\n }\n\n // Public\n toggle() {\n // Toggle class and sync the `aria-pressed` attribute with the return value of the `.toggle()` method\n this._element.setAttribute('aria-pressed', this._element.classList.toggle(CLASS_NAME_ACTIVE$3));\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Button.getOrCreateInstance(this);\n if (config === 'toggle') {\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$6, SELECTOR_DATA_TOGGLE$5, event => {\n event.preventDefault();\n const button = event.target.closest(SELECTOR_DATA_TOGGLE$5);\n const data = Button.getOrCreateInstance(button);\n data.toggle();\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Button);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/swipe.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$d = 'swipe';\nconst EVENT_KEY$9 = '.bs.swipe';\nconst EVENT_TOUCHSTART = `touchstart${EVENT_KEY$9}`;\nconst EVENT_TOUCHMOVE = `touchmove${EVENT_KEY$9}`;\nconst EVENT_TOUCHEND = `touchend${EVENT_KEY$9}`;\nconst EVENT_POINTERDOWN = `pointerdown${EVENT_KEY$9}`;\nconst EVENT_POINTERUP = `pointerup${EVENT_KEY$9}`;\nconst POINTER_TYPE_TOUCH = 'touch';\nconst POINTER_TYPE_PEN = 'pen';\nconst CLASS_NAME_POINTER_EVENT = 'pointer-event';\nconst SWIPE_THRESHOLD = 40;\nconst Default$c = {\n endCallback: null,\n leftCallback: null,\n rightCallback: null\n};\nconst DefaultType$c = {\n endCallback: '(function|null)',\n leftCallback: '(function|null)',\n rightCallback: '(function|null)'\n};\n\n/**\n * Class definition\n */\n\nclass Swipe extends Config {\n constructor(element, config) {\n super();\n this._element = element;\n if (!element || !Swipe.isSupported()) {\n return;\n }\n this._config = this._getConfig(config);\n this._deltaX = 0;\n this._supportPointerEvents = Boolean(window.PointerEvent);\n this._initEvents();\n }\n\n // Getters\n static get Default() {\n return Default$c;\n }\n static get DefaultType() {\n return DefaultType$c;\n }\n static get NAME() {\n return NAME$d;\n }\n\n // Public\n dispose() {\n EventHandler.off(this._element, EVENT_KEY$9);\n }\n\n // Private\n _start(event) {\n if (!this._supportPointerEvents) {\n this._deltaX = event.touches[0].clientX;\n return;\n }\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX;\n }\n }\n _end(event) {\n if (this._eventIsPointerPenTouch(event)) {\n this._deltaX = event.clientX - this._deltaX;\n }\n this._handleSwipe();\n execute(this._config.endCallback);\n }\n _move(event) {\n this._deltaX = event.touches && event.touches.length > 1 ? 0 : event.touches[0].clientX - this._deltaX;\n }\n _handleSwipe() {\n const absDeltaX = Math.abs(this._deltaX);\n if (absDeltaX <= SWIPE_THRESHOLD) {\n return;\n }\n const direction = absDeltaX / this._deltaX;\n this._deltaX = 0;\n if (!direction) {\n return;\n }\n execute(direction > 0 ? this._config.rightCallback : this._config.leftCallback);\n }\n _initEvents() {\n if (this._supportPointerEvents) {\n EventHandler.on(this._element, EVENT_POINTERDOWN, event => this._start(event));\n EventHandler.on(this._element, EVENT_POINTERUP, event => this._end(event));\n this._element.classList.add(CLASS_NAME_POINTER_EVENT);\n } else {\n EventHandler.on(this._element, EVENT_TOUCHSTART, event => this._start(event));\n EventHandler.on(this._element, EVENT_TOUCHMOVE, event => this._move(event));\n EventHandler.on(this._element, EVENT_TOUCHEND, event => this._end(event));\n }\n }\n _eventIsPointerPenTouch(event) {\n return this._supportPointerEvents && (event.pointerType === POINTER_TYPE_PEN || event.pointerType === POINTER_TYPE_TOUCH);\n }\n\n // Static\n static isSupported() {\n return 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$c = 'carousel';\nconst DATA_KEY$8 = 'bs.carousel';\nconst EVENT_KEY$8 = `.${DATA_KEY$8}`;\nconst DATA_API_KEY$5 = '.data-api';\nconst ARROW_LEFT_KEY$1 = 'ArrowLeft';\nconst ARROW_RIGHT_KEY$1 = 'ArrowRight';\nconst TOUCHEVENT_COMPAT_WAIT = 500; // Time for mouse compat events to fire after touch\n\nconst ORDER_NEXT = 'next';\nconst ORDER_PREV = 'prev';\nconst DIRECTION_LEFT = 'left';\nconst DIRECTION_RIGHT = 'right';\nconst EVENT_SLIDE = `slide${EVENT_KEY$8}`;\nconst EVENT_SLID = `slid${EVENT_KEY$8}`;\nconst EVENT_KEYDOWN$1 = `keydown${EVENT_KEY$8}`;\nconst EVENT_MOUSEENTER$1 = `mouseenter${EVENT_KEY$8}`;\nconst EVENT_MOUSELEAVE$1 = `mouseleave${EVENT_KEY$8}`;\nconst EVENT_DRAG_START = `dragstart${EVENT_KEY$8}`;\nconst EVENT_LOAD_DATA_API$3 = `load${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst EVENT_CLICK_DATA_API$5 = `click${EVENT_KEY$8}${DATA_API_KEY$5}`;\nconst CLASS_NAME_CAROUSEL = 'carousel';\nconst CLASS_NAME_ACTIVE$2 = 'active';\nconst CLASS_NAME_SLIDE = 'slide';\nconst CLASS_NAME_END = 'carousel-item-end';\nconst CLASS_NAME_START = 'carousel-item-start';\nconst CLASS_NAME_NEXT = 'carousel-item-next';\nconst CLASS_NAME_PREV = 'carousel-item-prev';\nconst SELECTOR_ACTIVE = '.active';\nconst SELECTOR_ITEM = '.carousel-item';\nconst SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM;\nconst SELECTOR_ITEM_IMG = '.carousel-item img';\nconst SELECTOR_INDICATORS = '.carousel-indicators';\nconst SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]';\nconst SELECTOR_DATA_RIDE = '[data-bs-ride=\"carousel\"]';\nconst KEY_TO_DIRECTION = {\n [ARROW_LEFT_KEY$1]: DIRECTION_RIGHT,\n [ARROW_RIGHT_KEY$1]: DIRECTION_LEFT\n};\nconst Default$b = {\n interval: 5000,\n keyboard: true,\n pause: 'hover',\n ride: false,\n touch: true,\n wrap: true\n};\nconst DefaultType$b = {\n interval: '(number|boolean)',\n // TODO:v6 remove boolean support\n keyboard: 'boolean',\n pause: '(string|boolean)',\n ride: '(boolean|string)',\n touch: 'boolean',\n wrap: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Carousel extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._interval = null;\n this._activeElement = null;\n this._isSliding = false;\n this.touchTimeout = null;\n this._swipeHelper = null;\n this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element);\n this._addEventListeners();\n if (this._config.ride === CLASS_NAME_CAROUSEL) {\n this.cycle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$b;\n }\n static get DefaultType() {\n return DefaultType$b;\n }\n static get NAME() {\n return NAME$c;\n }\n\n // Public\n next() {\n this._slide(ORDER_NEXT);\n }\n nextWhenVisible() {\n // FIXME TODO use `document.visibilityState`\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden && isVisible(this._element)) {\n this.next();\n }\n }\n prev() {\n this._slide(ORDER_PREV);\n }\n pause() {\n if (this._isSliding) {\n triggerTransitionEnd(this._element);\n }\n this._clearInterval();\n }\n cycle() {\n this._clearInterval();\n this._updateInterval();\n this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval);\n }\n _maybeEnableCycle() {\n if (!this._config.ride) {\n return;\n }\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.cycle());\n return;\n }\n this.cycle();\n }\n to(index) {\n const items = this._getItems();\n if (index > items.length - 1 || index < 0) {\n return;\n }\n if (this._isSliding) {\n EventHandler.one(this._element, EVENT_SLID, () => this.to(index));\n return;\n }\n const activeIndex = this._getItemIndex(this._getActive());\n if (activeIndex === index) {\n return;\n }\n const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV;\n this._slide(order, items[index]);\n }\n dispose() {\n if (this._swipeHelper) {\n this._swipeHelper.dispose();\n }\n super.dispose();\n }\n\n // Private\n _configAfterMerge(config) {\n config.defaultInterval = config.interval;\n return config;\n }\n _addEventListeners() {\n if (this._config.keyboard) {\n EventHandler.on(this._element, EVENT_KEYDOWN$1, event => this._keydown(event));\n }\n if (this._config.pause === 'hover') {\n EventHandler.on(this._element, EVENT_MOUSEENTER$1, () => this.pause());\n EventHandler.on(this._element, EVENT_MOUSELEAVE$1, () => this._maybeEnableCycle());\n }\n if (this._config.touch && Swipe.isSupported()) {\n this._addTouchEventListeners();\n }\n }\n _addTouchEventListeners() {\n for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {\n EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault());\n }\n const endCallBack = () => {\n if (this._config.pause !== 'hover') {\n return;\n }\n\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause();\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout);\n }\n this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval);\n };\n const swipeConfig = {\n leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),\n rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),\n endCallback: endCallBack\n };\n this._swipeHelper = new Swipe(this._element, swipeConfig);\n }\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return;\n }\n const direction = KEY_TO_DIRECTION[event.key];\n if (direction) {\n event.preventDefault();\n this._slide(this._directionToOrder(direction));\n }\n }\n _getItemIndex(element) {\n return this._getItems().indexOf(element);\n }\n _setActiveIndicatorElement(index) {\n if (!this._indicatorsElement) {\n return;\n }\n const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement);\n activeIndicator.classList.remove(CLASS_NAME_ACTIVE$2);\n activeIndicator.removeAttribute('aria-current');\n const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to=\"${index}\"]`, this._indicatorsElement);\n if (newActiveIndicator) {\n newActiveIndicator.classList.add(CLASS_NAME_ACTIVE$2);\n newActiveIndicator.setAttribute('aria-current', 'true');\n }\n }\n _updateInterval() {\n const element = this._activeElement || this._getActive();\n if (!element) {\n return;\n }\n const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10);\n this._config.interval = elementInterval || this._config.defaultInterval;\n }\n _slide(order, element = null) {\n if (this._isSliding) {\n return;\n }\n const activeElement = this._getActive();\n const isNext = order === ORDER_NEXT;\n const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap);\n if (nextElement === activeElement) {\n return;\n }\n const nextElementIndex = this._getItemIndex(nextElement);\n const triggerEvent = eventName => {\n return EventHandler.trigger(this._element, eventName, {\n relatedTarget: nextElement,\n direction: this._orderToDirection(order),\n from: this._getItemIndex(activeElement),\n to: nextElementIndex\n });\n };\n const slideEvent = triggerEvent(EVENT_SLIDE);\n if (slideEvent.defaultPrevented) {\n return;\n }\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n // TODO: change tests that use empty divs to avoid this check\n return;\n }\n const isCycling = Boolean(this._interval);\n this.pause();\n this._isSliding = true;\n this._setActiveIndicatorElement(nextElementIndex);\n this._activeElement = nextElement;\n const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END;\n const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV;\n nextElement.classList.add(orderClassName);\n reflow(nextElement);\n activeElement.classList.add(directionalClassName);\n nextElement.classList.add(directionalClassName);\n const completeCallBack = () => {\n nextElement.classList.remove(directionalClassName, orderClassName);\n nextElement.classList.add(CLASS_NAME_ACTIVE$2);\n activeElement.classList.remove(CLASS_NAME_ACTIVE$2, orderClassName, directionalClassName);\n this._isSliding = false;\n triggerEvent(EVENT_SLID);\n };\n this._queueCallback(completeCallBack, activeElement, this._isAnimated());\n if (isCycling) {\n this.cycle();\n }\n }\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_SLIDE);\n }\n _getActive() {\n return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element);\n }\n _getItems() {\n return SelectorEngine.find(SELECTOR_ITEM, this._element);\n }\n _clearInterval() {\n if (this._interval) {\n clearInterval(this._interval);\n this._interval = null;\n }\n }\n _directionToOrder(direction) {\n if (isRTL()) {\n return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT;\n }\n return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV;\n }\n _orderToDirection(order) {\n if (isRTL()) {\n return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT;\n }\n return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT;\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Carousel.getOrCreateInstance(this, config);\n if (typeof config === 'number') {\n data.to(config);\n return;\n }\n if (typeof config === 'string') {\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$5, SELECTOR_DATA_SLIDE, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {\n return;\n }\n event.preventDefault();\n const carousel = Carousel.getOrCreateInstance(target);\n const slideIndex = this.getAttribute('data-bs-slide-to');\n if (slideIndex) {\n carousel.to(slideIndex);\n carousel._maybeEnableCycle();\n return;\n }\n if (Manipulator.getDataAttribute(this, 'slide') === 'next') {\n carousel.next();\n carousel._maybeEnableCycle();\n return;\n }\n carousel.prev();\n carousel._maybeEnableCycle();\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$3, () => {\n const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE);\n for (const carousel of carousels) {\n Carousel.getOrCreateInstance(carousel);\n }\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Carousel);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$b = 'collapse';\nconst DATA_KEY$7 = 'bs.collapse';\nconst EVENT_KEY$7 = `.${DATA_KEY$7}`;\nconst DATA_API_KEY$4 = '.data-api';\nconst EVENT_SHOW$6 = `show${EVENT_KEY$7}`;\nconst EVENT_SHOWN$6 = `shown${EVENT_KEY$7}`;\nconst EVENT_HIDE$6 = `hide${EVENT_KEY$7}`;\nconst EVENT_HIDDEN$6 = `hidden${EVENT_KEY$7}`;\nconst EVENT_CLICK_DATA_API$4 = `click${EVENT_KEY$7}${DATA_API_KEY$4}`;\nconst CLASS_NAME_SHOW$7 = 'show';\nconst CLASS_NAME_COLLAPSE = 'collapse';\nconst CLASS_NAME_COLLAPSING = 'collapsing';\nconst CLASS_NAME_COLLAPSED = 'collapsed';\nconst CLASS_NAME_DEEPER_CHILDREN = `:scope .${CLASS_NAME_COLLAPSE} .${CLASS_NAME_COLLAPSE}`;\nconst CLASS_NAME_HORIZONTAL = 'collapse-horizontal';\nconst WIDTH = 'width';\nconst HEIGHT = 'height';\nconst SELECTOR_ACTIVES = '.collapse.show, .collapse.collapsing';\nconst SELECTOR_DATA_TOGGLE$4 = '[data-bs-toggle=\"collapse\"]';\nconst Default$a = {\n parent: null,\n toggle: true\n};\nconst DefaultType$a = {\n parent: '(null|element)',\n toggle: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Collapse extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isTransitioning = false;\n this._triggerArray = [];\n const toggleList = SelectorEngine.find(SELECTOR_DATA_TOGGLE$4);\n for (const elem of toggleList) {\n const selector = SelectorEngine.getSelectorFromElement(elem);\n const filterElement = SelectorEngine.find(selector).filter(foundElement => foundElement === this._element);\n if (selector !== null && filterElement.length) {\n this._triggerArray.push(elem);\n }\n }\n this._initializeChildren();\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._triggerArray, this._isShown());\n }\n if (this._config.toggle) {\n this.toggle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$a;\n }\n static get DefaultType() {\n return DefaultType$a;\n }\n static get NAME() {\n return NAME$b;\n }\n\n // Public\n toggle() {\n if (this._isShown()) {\n this.hide();\n } else {\n this.show();\n }\n }\n show() {\n if (this._isTransitioning || this._isShown()) {\n return;\n }\n let activeChildren = [];\n\n // find active children\n if (this._config.parent) {\n activeChildren = this._getFirstLevelChildren(SELECTOR_ACTIVES).filter(element => element !== this._element).map(element => Collapse.getOrCreateInstance(element, {\n toggle: false\n }));\n }\n if (activeChildren.length && activeChildren[0]._isTransitioning) {\n return;\n }\n const startEvent = EventHandler.trigger(this._element, EVENT_SHOW$6);\n if (startEvent.defaultPrevented) {\n return;\n }\n for (const activeInstance of activeChildren) {\n activeInstance.hide();\n }\n const dimension = this._getDimension();\n this._element.classList.remove(CLASS_NAME_COLLAPSE);\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n this._element.style[dimension] = 0;\n this._addAriaAndCollapsedClass(this._triggerArray, true);\n this._isTransitioning = true;\n const complete = () => {\n this._isTransitioning = false;\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n this._element.classList.add(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n this._element.style[dimension] = '';\n EventHandler.trigger(this._element, EVENT_SHOWN$6);\n };\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1);\n const scrollSize = `scroll${capitalizedDimension}`;\n this._queueCallback(complete, this._element, true);\n this._element.style[dimension] = `${this._element[scrollSize]}px`;\n }\n hide() {\n if (this._isTransitioning || !this._isShown()) {\n return;\n }\n const startEvent = EventHandler.trigger(this._element, EVENT_HIDE$6);\n if (startEvent.defaultPrevented) {\n return;\n }\n const dimension = this._getDimension();\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`;\n reflow(this._element);\n this._element.classList.add(CLASS_NAME_COLLAPSING);\n this._element.classList.remove(CLASS_NAME_COLLAPSE, CLASS_NAME_SHOW$7);\n for (const trigger of this._triggerArray) {\n const element = SelectorEngine.getElementFromSelector(trigger);\n if (element && !this._isShown(element)) {\n this._addAriaAndCollapsedClass([trigger], false);\n }\n }\n this._isTransitioning = true;\n const complete = () => {\n this._isTransitioning = false;\n this._element.classList.remove(CLASS_NAME_COLLAPSING);\n this._element.classList.add(CLASS_NAME_COLLAPSE);\n EventHandler.trigger(this._element, EVENT_HIDDEN$6);\n };\n this._element.style[dimension] = '';\n this._queueCallback(complete, this._element, true);\n }\n _isShown(element = this._element) {\n return element.classList.contains(CLASS_NAME_SHOW$7);\n }\n\n // Private\n _configAfterMerge(config) {\n config.toggle = Boolean(config.toggle); // Coerce string values\n config.parent = getElement(config.parent);\n return config;\n }\n _getDimension() {\n return this._element.classList.contains(CLASS_NAME_HORIZONTAL) ? WIDTH : HEIGHT;\n }\n _initializeChildren() {\n if (!this._config.parent) {\n return;\n }\n const children = this._getFirstLevelChildren(SELECTOR_DATA_TOGGLE$4);\n for (const element of children) {\n const selected = SelectorEngine.getElementFromSelector(element);\n if (selected) {\n this._addAriaAndCollapsedClass([element], this._isShown(selected));\n }\n }\n }\n _getFirstLevelChildren(selector) {\n const children = SelectorEngine.find(CLASS_NAME_DEEPER_CHILDREN, this._config.parent);\n // remove children if greater depth\n return SelectorEngine.find(selector, this._config.parent).filter(element => !children.includes(element));\n }\n _addAriaAndCollapsedClass(triggerArray, isOpen) {\n if (!triggerArray.length) {\n return;\n }\n for (const element of triggerArray) {\n element.classList.toggle(CLASS_NAME_COLLAPSED, !isOpen);\n element.setAttribute('aria-expanded', isOpen);\n }\n }\n\n // Static\n static jQueryInterface(config) {\n const _config = {};\n if (typeof config === 'string' && /show|hide/.test(config)) {\n _config.toggle = false;\n }\n return this.each(function () {\n const data = Collapse.getOrCreateInstance(this, _config);\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n }\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$4, SELECTOR_DATA_TOGGLE$4, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.target.tagName === 'A' || event.delegateTarget && event.delegateTarget.tagName === 'A') {\n event.preventDefault();\n }\n for (const element of SelectorEngine.getMultipleElementsFromSelector(this)) {\n Collapse.getOrCreateInstance(element, {\n toggle: false\n }).toggle();\n }\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Collapse);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$a = 'dropdown';\nconst DATA_KEY$6 = 'bs.dropdown';\nconst EVENT_KEY$6 = `.${DATA_KEY$6}`;\nconst DATA_API_KEY$3 = '.data-api';\nconst ESCAPE_KEY$2 = 'Escape';\nconst TAB_KEY$1 = 'Tab';\nconst ARROW_UP_KEY$1 = 'ArrowUp';\nconst ARROW_DOWN_KEY$1 = 'ArrowDown';\nconst RIGHT_MOUSE_BUTTON = 2; // MouseEvent.button value for the secondary button, usually the right button\n\nconst EVENT_HIDE$5 = `hide${EVENT_KEY$6}`;\nconst EVENT_HIDDEN$5 = `hidden${EVENT_KEY$6}`;\nconst EVENT_SHOW$5 = `show${EVENT_KEY$6}`;\nconst EVENT_SHOWN$5 = `shown${EVENT_KEY$6}`;\nconst EVENT_CLICK_DATA_API$3 = `click${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY$6}${DATA_API_KEY$3}`;\nconst CLASS_NAME_SHOW$6 = 'show';\nconst CLASS_NAME_DROPUP = 'dropup';\nconst CLASS_NAME_DROPEND = 'dropend';\nconst CLASS_NAME_DROPSTART = 'dropstart';\nconst CLASS_NAME_DROPUP_CENTER = 'dropup-center';\nconst CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center';\nconst SELECTOR_DATA_TOGGLE$3 = '[data-bs-toggle=\"dropdown\"]:not(.disabled):not(:disabled)';\nconst SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE$3}.${CLASS_NAME_SHOW$6}`;\nconst SELECTOR_MENU = '.dropdown-menu';\nconst SELECTOR_NAVBAR = '.navbar';\nconst SELECTOR_NAVBAR_NAV = '.navbar-nav';\nconst SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)';\nconst PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start';\nconst PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end';\nconst PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start';\nconst PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end';\nconst PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start';\nconst PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start';\nconst PLACEMENT_TOPCENTER = 'top';\nconst PLACEMENT_BOTTOMCENTER = 'bottom';\nconst Default$9 = {\n autoClose: true,\n boundary: 'clippingParents',\n display: 'dynamic',\n offset: [0, 2],\n popperConfig: null,\n reference: 'toggle'\n};\nconst DefaultType$9 = {\n autoClose: '(boolean|string)',\n boundary: '(string|element)',\n display: 'string',\n offset: '(array|string|function)',\n popperConfig: '(null|object|function)',\n reference: '(string|element|object)'\n};\n\n/**\n * Class definition\n */\n\nclass Dropdown extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._popper = null;\n this._parent = this._element.parentNode; // dropdown wrapper\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] || SelectorEngine.prev(this._element, SELECTOR_MENU)[0] || SelectorEngine.findOne(SELECTOR_MENU, this._parent);\n this._inNavbar = this._detectNavbar();\n }\n\n // Getters\n static get Default() {\n return Default$9;\n }\n static get DefaultType() {\n return DefaultType$9;\n }\n static get NAME() {\n return NAME$a;\n }\n\n // Public\n toggle() {\n return this._isShown() ? this.hide() : this.show();\n }\n show() {\n if (isDisabled(this._element) || this._isShown()) {\n return;\n }\n const relatedTarget = {\n relatedTarget: this._element\n };\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$5, relatedTarget);\n if (showEvent.defaultPrevented) {\n return;\n }\n this._createPopper();\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n this._element.focus();\n this._element.setAttribute('aria-expanded', true);\n this._menu.classList.add(CLASS_NAME_SHOW$6);\n this._element.classList.add(CLASS_NAME_SHOW$6);\n EventHandler.trigger(this._element, EVENT_SHOWN$5, relatedTarget);\n }\n hide() {\n if (isDisabled(this._element) || !this._isShown()) {\n return;\n }\n const relatedTarget = {\n relatedTarget: this._element\n };\n this._completeHide(relatedTarget);\n }\n dispose() {\n if (this._popper) {\n this._popper.destroy();\n }\n super.dispose();\n }\n update() {\n this._inNavbar = this._detectNavbar();\n if (this._popper) {\n this._popper.update();\n }\n }\n\n // Private\n _completeHide(relatedTarget) {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$5, relatedTarget);\n if (hideEvent.defaultPrevented) {\n return;\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n if (this._popper) {\n this._popper.destroy();\n }\n this._menu.classList.remove(CLASS_NAME_SHOW$6);\n this._element.classList.remove(CLASS_NAME_SHOW$6);\n this._element.setAttribute('aria-expanded', 'false');\n Manipulator.removeDataAttribute(this._menu, 'popper');\n EventHandler.trigger(this._element, EVENT_HIDDEN$5, relatedTarget);\n }\n _getConfig(config) {\n config = super._getConfig(config);\n if (typeof config.reference === 'object' && !isElement(config.reference) && typeof config.reference.getBoundingClientRect !== 'function') {\n // Popper virtual elements require a getBoundingClientRect method\n throw new TypeError(`${NAME$a.toUpperCase()}: Option \"reference\" provided type \"object\" without a required \"getBoundingClientRect\" method.`);\n }\n return config;\n }\n _createPopper() {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper (https://popper.js.org)');\n }\n let referenceElement = this._element;\n if (this._config.reference === 'parent') {\n referenceElement = this._parent;\n } else if (isElement(this._config.reference)) {\n referenceElement = getElement(this._config.reference);\n } else if (typeof this._config.reference === 'object') {\n referenceElement = this._config.reference;\n }\n const popperConfig = this._getPopperConfig();\n this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig);\n }\n _isShown() {\n return this._menu.classList.contains(CLASS_NAME_SHOW$6);\n }\n _getPlacement() {\n const parentDropdown = this._parent;\n if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {\n return PLACEMENT_RIGHT;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {\n return PLACEMENT_LEFT;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {\n return PLACEMENT_TOPCENTER;\n }\n if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {\n return PLACEMENT_BOTTOMCENTER;\n }\n\n // We need to trim the value because custom properties can also include spaces\n const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end';\n if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {\n return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP;\n }\n return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM;\n }\n _detectNavbar() {\n return this._element.closest(SELECTOR_NAVBAR) !== null;\n }\n _getOffset() {\n const {\n offset\n } = this._config;\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n return offset;\n }\n _getPopperConfig() {\n const defaultBsPopperConfig = {\n placement: this._getPlacement(),\n modifiers: [{\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }]\n };\n\n // Disable Popper if we have a static display or Dropdown is in Navbar\n if (this._inNavbar || this._config.display === 'static') {\n Manipulator.setDataAttribute(this._menu, 'popper', 'static'); // TODO: v6 remove\n defaultBsPopperConfig.modifiers = [{\n name: 'applyStyles',\n enabled: false\n }];\n }\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n };\n }\n _selectMenuItem({\n key,\n target\n }) {\n const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element));\n if (!items.length) {\n return;\n }\n\n // if target isn't included in items (e.g. when expanding the dropdown)\n // allow cycling to get the last item in case key equals ARROW_UP_KEY\n getNextActiveElement(items, target, key === ARROW_DOWN_KEY$1, !items.includes(target)).focus();\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Dropdown.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n static clearMenus(event) {\n if (event.button === RIGHT_MOUSE_BUTTON || event.type === 'keyup' && event.key !== TAB_KEY$1) {\n return;\n }\n const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN);\n for (const toggle of openToggles) {\n const context = Dropdown.getInstance(toggle);\n if (!context || context._config.autoClose === false) {\n continue;\n }\n const composedPath = event.composedPath();\n const isMenuTarget = composedPath.includes(context._menu);\n if (composedPath.includes(context._element) || context._config.autoClose === 'inside' && !isMenuTarget || context._config.autoClose === 'outside' && isMenuTarget) {\n continue;\n }\n\n // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu\n if (context._menu.contains(event.target) && (event.type === 'keyup' && event.key === TAB_KEY$1 || /input|select|option|textarea|form/i.test(event.target.tagName))) {\n continue;\n }\n const relatedTarget = {\n relatedTarget: context._element\n };\n if (event.type === 'click') {\n relatedTarget.clickEvent = event;\n }\n context._completeHide(relatedTarget);\n }\n }\n static dataApiKeydownHandler(event) {\n // If not an UP | DOWN | ESCAPE key => not a dropdown command\n // If input/textarea && if key is other than ESCAPE => not a dropdown command\n\n const isInput = /input|textarea/i.test(event.target.tagName);\n const isEscapeEvent = event.key === ESCAPE_KEY$2;\n const isUpOrDownEvent = [ARROW_UP_KEY$1, ARROW_DOWN_KEY$1].includes(event.key);\n if (!isUpOrDownEvent && !isEscapeEvent) {\n return;\n }\n if (isInput && !isEscapeEvent) {\n return;\n }\n event.preventDefault();\n\n // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/\n const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE$3) ? this : SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.next(this, SELECTOR_DATA_TOGGLE$3)[0] || SelectorEngine.findOne(SELECTOR_DATA_TOGGLE$3, event.delegateTarget.parentNode);\n const instance = Dropdown.getOrCreateInstance(getToggleButton);\n if (isUpOrDownEvent) {\n event.stopPropagation();\n instance.show();\n instance._selectMenuItem(event);\n return;\n }\n if (instance._isShown()) {\n // else is escape and we check if it is shown\n event.stopPropagation();\n instance.hide();\n getToggleButton.focus();\n }\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE$3, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus);\nEventHandler.on(document, EVENT_CLICK_DATA_API$3, SELECTOR_DATA_TOGGLE$3, function (event) {\n event.preventDefault();\n Dropdown.getOrCreateInstance(this).toggle();\n});\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Dropdown);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/backdrop.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$9 = 'backdrop';\nconst CLASS_NAME_FADE$4 = 'fade';\nconst CLASS_NAME_SHOW$5 = 'show';\nconst EVENT_MOUSEDOWN = `mousedown.bs.${NAME$9}`;\nconst Default$8 = {\n className: 'modal-backdrop',\n clickCallback: null,\n isAnimated: false,\n isVisible: true,\n // if false, we use the backdrop helper without adding any element to the dom\n rootElement: 'body' // give the choice to place backdrop under different elements\n};\n\nconst DefaultType$8 = {\n className: 'string',\n clickCallback: '(function|null)',\n isAnimated: 'boolean',\n isVisible: 'boolean',\n rootElement: '(element|string)'\n};\n\n/**\n * Class definition\n */\n\nclass Backdrop extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isAppended = false;\n this._element = null;\n }\n\n // Getters\n static get Default() {\n return Default$8;\n }\n static get DefaultType() {\n return DefaultType$8;\n }\n static get NAME() {\n return NAME$9;\n }\n\n // Public\n show(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n this._append();\n const element = this._getElement();\n if (this._config.isAnimated) {\n reflow(element);\n }\n element.classList.add(CLASS_NAME_SHOW$5);\n this._emulateAnimation(() => {\n execute(callback);\n });\n }\n hide(callback) {\n if (!this._config.isVisible) {\n execute(callback);\n return;\n }\n this._getElement().classList.remove(CLASS_NAME_SHOW$5);\n this._emulateAnimation(() => {\n this.dispose();\n execute(callback);\n });\n }\n dispose() {\n if (!this._isAppended) {\n return;\n }\n EventHandler.off(this._element, EVENT_MOUSEDOWN);\n this._element.remove();\n this._isAppended = false;\n }\n\n // Private\n _getElement() {\n if (!this._element) {\n const backdrop = document.createElement('div');\n backdrop.className = this._config.className;\n if (this._config.isAnimated) {\n backdrop.classList.add(CLASS_NAME_FADE$4);\n }\n this._element = backdrop;\n }\n return this._element;\n }\n _configAfterMerge(config) {\n // use getElement() with the default \"body\" to get a fresh Element on each instantiation\n config.rootElement = getElement(config.rootElement);\n return config;\n }\n _append() {\n if (this._isAppended) {\n return;\n }\n const element = this._getElement();\n this._config.rootElement.append(element);\n EventHandler.on(element, EVENT_MOUSEDOWN, () => {\n execute(this._config.clickCallback);\n });\n this._isAppended = true;\n }\n _emulateAnimation(callback) {\n executeAfterTransition(callback, this._getElement(), this._config.isAnimated);\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/focustrap.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$8 = 'focustrap';\nconst DATA_KEY$5 = 'bs.focustrap';\nconst EVENT_KEY$5 = `.${DATA_KEY$5}`;\nconst EVENT_FOCUSIN$2 = `focusin${EVENT_KEY$5}`;\nconst EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY$5}`;\nconst TAB_KEY = 'Tab';\nconst TAB_NAV_FORWARD = 'forward';\nconst TAB_NAV_BACKWARD = 'backward';\nconst Default$7 = {\n autofocus: true,\n trapElement: null // The element to trap focus inside of\n};\n\nconst DefaultType$7 = {\n autofocus: 'boolean',\n trapElement: 'element'\n};\n\n/**\n * Class definition\n */\n\nclass FocusTrap extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n this._isActive = false;\n this._lastTabNavDirection = null;\n }\n\n // Getters\n static get Default() {\n return Default$7;\n }\n static get DefaultType() {\n return DefaultType$7;\n }\n static get NAME() {\n return NAME$8;\n }\n\n // Public\n activate() {\n if (this._isActive) {\n return;\n }\n if (this._config.autofocus) {\n this._config.trapElement.focus();\n }\n EventHandler.off(document, EVENT_KEY$5); // guard against infinite focus loop\n EventHandler.on(document, EVENT_FOCUSIN$2, event => this._handleFocusin(event));\n EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event));\n this._isActive = true;\n }\n deactivate() {\n if (!this._isActive) {\n return;\n }\n this._isActive = false;\n EventHandler.off(document, EVENT_KEY$5);\n }\n\n // Private\n _handleFocusin(event) {\n const {\n trapElement\n } = this._config;\n if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {\n return;\n }\n const elements = SelectorEngine.focusableChildren(trapElement);\n if (elements.length === 0) {\n trapElement.focus();\n } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {\n elements[elements.length - 1].focus();\n } else {\n elements[0].focus();\n }\n }\n _handleKeydown(event) {\n if (event.key !== TAB_KEY) {\n return;\n }\n this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/scrollBar.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst SELECTOR_FIXED_CONTENT = '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top';\nconst SELECTOR_STICKY_CONTENT = '.sticky-top';\nconst PROPERTY_PADDING = 'padding-right';\nconst PROPERTY_MARGIN = 'margin-right';\n\n/**\n * Class definition\n */\n\nclass ScrollBarHelper {\n constructor() {\n this._element = document.body;\n }\n\n // Public\n getWidth() {\n // https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth#usage_notes\n const documentWidth = document.documentElement.clientWidth;\n return Math.abs(window.innerWidth - documentWidth);\n }\n hide() {\n const width = this.getWidth();\n this._disableOverFlow();\n // give padding to element to balance the hidden scrollbar width\n this._setElementAttributes(this._element, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n // trick: We adjust positive paddingRight and negative marginRight to sticky-top elements to keep showing fullwidth\n this._setElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING, calculatedValue => calculatedValue + width);\n this._setElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN, calculatedValue => calculatedValue - width);\n }\n reset() {\n this._resetElementAttributes(this._element, 'overflow');\n this._resetElementAttributes(this._element, PROPERTY_PADDING);\n this._resetElementAttributes(SELECTOR_FIXED_CONTENT, PROPERTY_PADDING);\n this._resetElementAttributes(SELECTOR_STICKY_CONTENT, PROPERTY_MARGIN);\n }\n isOverflowing() {\n return this.getWidth() > 0;\n }\n\n // Private\n _disableOverFlow() {\n this._saveInitialAttribute(this._element, 'overflow');\n this._element.style.overflow = 'hidden';\n }\n _setElementAttributes(selector, styleProperty, callback) {\n const scrollbarWidth = this.getWidth();\n const manipulationCallBack = element => {\n if (element !== this._element && window.innerWidth > element.clientWidth + scrollbarWidth) {\n return;\n }\n this._saveInitialAttribute(element, styleProperty);\n const calculatedValue = window.getComputedStyle(element).getPropertyValue(styleProperty);\n element.style.setProperty(styleProperty, `${callback(Number.parseFloat(calculatedValue))}px`);\n };\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n _saveInitialAttribute(element, styleProperty) {\n const actualValue = element.style.getPropertyValue(styleProperty);\n if (actualValue) {\n Manipulator.setDataAttribute(element, styleProperty, actualValue);\n }\n }\n _resetElementAttributes(selector, styleProperty) {\n const manipulationCallBack = element => {\n const value = Manipulator.getDataAttribute(element, styleProperty);\n // We only want to remove the property if the value is `null`; the value can also be zero\n if (value === null) {\n element.style.removeProperty(styleProperty);\n return;\n }\n Manipulator.removeDataAttribute(element, styleProperty);\n element.style.setProperty(styleProperty, value);\n };\n this._applyManipulationCallback(selector, manipulationCallBack);\n }\n _applyManipulationCallback(selector, callBack) {\n if (isElement(selector)) {\n callBack(selector);\n return;\n }\n for (const sel of SelectorEngine.find(selector, this._element)) {\n callBack(sel);\n }\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$7 = 'modal';\nconst DATA_KEY$4 = 'bs.modal';\nconst EVENT_KEY$4 = `.${DATA_KEY$4}`;\nconst DATA_API_KEY$2 = '.data-api';\nconst ESCAPE_KEY$1 = 'Escape';\nconst EVENT_HIDE$4 = `hide${EVENT_KEY$4}`;\nconst EVENT_HIDE_PREVENTED$1 = `hidePrevented${EVENT_KEY$4}`;\nconst EVENT_HIDDEN$4 = `hidden${EVENT_KEY$4}`;\nconst EVENT_SHOW$4 = `show${EVENT_KEY$4}`;\nconst EVENT_SHOWN$4 = `shown${EVENT_KEY$4}`;\nconst EVENT_RESIZE$1 = `resize${EVENT_KEY$4}`;\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY$4}`;\nconst EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY$4}`;\nconst EVENT_KEYDOWN_DISMISS$1 = `keydown.dismiss${EVENT_KEY$4}`;\nconst EVENT_CLICK_DATA_API$2 = `click${EVENT_KEY$4}${DATA_API_KEY$2}`;\nconst CLASS_NAME_OPEN = 'modal-open';\nconst CLASS_NAME_FADE$3 = 'fade';\nconst CLASS_NAME_SHOW$4 = 'show';\nconst CLASS_NAME_STATIC = 'modal-static';\nconst OPEN_SELECTOR$1 = '.modal.show';\nconst SELECTOR_DIALOG = '.modal-dialog';\nconst SELECTOR_MODAL_BODY = '.modal-body';\nconst SELECTOR_DATA_TOGGLE$2 = '[data-bs-toggle=\"modal\"]';\nconst Default$6 = {\n backdrop: true,\n focus: true,\n keyboard: true\n};\nconst DefaultType$6 = {\n backdrop: '(boolean|string)',\n focus: 'boolean',\n keyboard: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Modal extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element);\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n this._isShown = false;\n this._isTransitioning = false;\n this._scrollBar = new ScrollBarHelper();\n this._addEventListeners();\n }\n\n // Getters\n static get Default() {\n return Default$6;\n }\n static get DefaultType() {\n return DefaultType$6;\n }\n static get NAME() {\n return NAME$7;\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$4, {\n relatedTarget\n });\n if (showEvent.defaultPrevented) {\n return;\n }\n this._isShown = true;\n this._isTransitioning = true;\n this._scrollBar.hide();\n document.body.classList.add(CLASS_NAME_OPEN);\n this._adjustDialog();\n this._backdrop.show(() => this._showElement(relatedTarget));\n }\n hide() {\n if (!this._isShown || this._isTransitioning) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$4);\n if (hideEvent.defaultPrevented) {\n return;\n }\n this._isShown = false;\n this._isTransitioning = true;\n this._focustrap.deactivate();\n this._element.classList.remove(CLASS_NAME_SHOW$4);\n this._queueCallback(() => this._hideModal(), this._element, this._isAnimated());\n }\n dispose() {\n EventHandler.off(window, EVENT_KEY$4);\n EventHandler.off(this._dialog, EVENT_KEY$4);\n this._backdrop.dispose();\n this._focustrap.deactivate();\n super.dispose();\n }\n handleUpdate() {\n this._adjustDialog();\n }\n\n // Private\n _initializeBackDrop() {\n return new Backdrop({\n isVisible: Boolean(this._config.backdrop),\n // 'static' option will be translated to true, and booleans will keep their value,\n isAnimated: this._isAnimated()\n });\n }\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n _showElement(relatedTarget) {\n // try to append dynamic modal\n if (!document.body.contains(this._element)) {\n document.body.append(this._element);\n }\n this._element.style.display = 'block';\n this._element.removeAttribute('aria-hidden');\n this._element.setAttribute('aria-modal', true);\n this._element.setAttribute('role', 'dialog');\n this._element.scrollTop = 0;\n const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog);\n if (modalBody) {\n modalBody.scrollTop = 0;\n }\n reflow(this._element);\n this._element.classList.add(CLASS_NAME_SHOW$4);\n const transitionComplete = () => {\n if (this._config.focus) {\n this._focustrap.activate();\n }\n this._isTransitioning = false;\n EventHandler.trigger(this._element, EVENT_SHOWN$4, {\n relatedTarget\n });\n };\n this._queueCallback(transitionComplete, this._dialog, this._isAnimated());\n }\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS$1, event => {\n if (event.key !== ESCAPE_KEY$1) {\n return;\n }\n if (this._config.keyboard) {\n this.hide();\n return;\n }\n this._triggerBackdropTransition();\n });\n EventHandler.on(window, EVENT_RESIZE$1, () => {\n if (this._isShown && !this._isTransitioning) {\n this._adjustDialog();\n }\n });\n EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {\n // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks\n EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {\n if (this._element !== event.target || this._element !== event2.target) {\n return;\n }\n if (this._config.backdrop === 'static') {\n this._triggerBackdropTransition();\n return;\n }\n if (this._config.backdrop) {\n this.hide();\n }\n });\n });\n }\n _hideModal() {\n this._element.style.display = 'none';\n this._element.setAttribute('aria-hidden', true);\n this._element.removeAttribute('aria-modal');\n this._element.removeAttribute('role');\n this._isTransitioning = false;\n this._backdrop.hide(() => {\n document.body.classList.remove(CLASS_NAME_OPEN);\n this._resetAdjustments();\n this._scrollBar.reset();\n EventHandler.trigger(this._element, EVENT_HIDDEN$4);\n });\n }\n _isAnimated() {\n return this._element.classList.contains(CLASS_NAME_FADE$3);\n }\n _triggerBackdropTransition() {\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED$1);\n if (hideEvent.defaultPrevented) {\n return;\n }\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n const initialOverflowY = this._element.style.overflowY;\n // return if the following background transition hasn't yet completed\n if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {\n return;\n }\n if (!isModalOverflowing) {\n this._element.style.overflowY = 'hidden';\n }\n this._element.classList.add(CLASS_NAME_STATIC);\n this._queueCallback(() => {\n this._element.classList.remove(CLASS_NAME_STATIC);\n this._queueCallback(() => {\n this._element.style.overflowY = initialOverflowY;\n }, this._dialog);\n }, this._dialog);\n this._element.focus();\n }\n\n /**\n * The following methods are used to handle overflowing modals\n */\n\n _adjustDialog() {\n const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;\n const scrollbarWidth = this._scrollBar.getWidth();\n const isBodyOverflowing = scrollbarWidth > 0;\n if (isBodyOverflowing && !isModalOverflowing) {\n const property = isRTL() ? 'paddingLeft' : 'paddingRight';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n if (!isBodyOverflowing && isModalOverflowing) {\n const property = isRTL() ? 'paddingRight' : 'paddingLeft';\n this._element.style[property] = `${scrollbarWidth}px`;\n }\n }\n _resetAdjustments() {\n this._element.style.paddingLeft = '';\n this._element.style.paddingRight = '';\n }\n\n // Static\n static jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n const data = Modal.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](relatedTarget);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$2, SELECTOR_DATA_TOGGLE$2, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n EventHandler.one(target, EVENT_SHOW$4, showEvent => {\n if (showEvent.defaultPrevented) {\n // only register focus restorer if modal will actually get shown\n return;\n }\n EventHandler.one(target, EVENT_HIDDEN$4, () => {\n if (isVisible(this)) {\n this.focus();\n }\n });\n });\n\n // avoid conflict when clicking modal toggler while another one is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR$1);\n if (alreadyOpen) {\n Modal.getInstance(alreadyOpen).hide();\n }\n const data = Modal.getOrCreateInstance(target);\n data.toggle(this);\n});\nenableDismissTrigger(Modal);\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Modal);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap offcanvas.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$6 = 'offcanvas';\nconst DATA_KEY$3 = 'bs.offcanvas';\nconst EVENT_KEY$3 = `.${DATA_KEY$3}`;\nconst DATA_API_KEY$1 = '.data-api';\nconst EVENT_LOAD_DATA_API$2 = `load${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst ESCAPE_KEY = 'Escape';\nconst CLASS_NAME_SHOW$3 = 'show';\nconst CLASS_NAME_SHOWING$1 = 'showing';\nconst CLASS_NAME_HIDING = 'hiding';\nconst CLASS_NAME_BACKDROP = 'offcanvas-backdrop';\nconst OPEN_SELECTOR = '.offcanvas.show';\nconst EVENT_SHOW$3 = `show${EVENT_KEY$3}`;\nconst EVENT_SHOWN$3 = `shown${EVENT_KEY$3}`;\nconst EVENT_HIDE$3 = `hide${EVENT_KEY$3}`;\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY$3}`;\nconst EVENT_HIDDEN$3 = `hidden${EVENT_KEY$3}`;\nconst EVENT_RESIZE = `resize${EVENT_KEY$3}`;\nconst EVENT_CLICK_DATA_API$1 = `click${EVENT_KEY$3}${DATA_API_KEY$1}`;\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY$3}`;\nconst SELECTOR_DATA_TOGGLE$1 = '[data-bs-toggle=\"offcanvas\"]';\nconst Default$5 = {\n backdrop: true,\n keyboard: true,\n scroll: false\n};\nconst DefaultType$5 = {\n backdrop: '(boolean|string)',\n keyboard: 'boolean',\n scroll: 'boolean'\n};\n\n/**\n * Class definition\n */\n\nclass Offcanvas extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n this._isShown = false;\n this._backdrop = this._initializeBackDrop();\n this._focustrap = this._initializeFocusTrap();\n this._addEventListeners();\n }\n\n // Getters\n static get Default() {\n return Default$5;\n }\n static get DefaultType() {\n return DefaultType$5;\n }\n static get NAME() {\n return NAME$6;\n }\n\n // Public\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget);\n }\n show(relatedTarget) {\n if (this._isShown) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, EVENT_SHOW$3, {\n relatedTarget\n });\n if (showEvent.defaultPrevented) {\n return;\n }\n this._isShown = true;\n this._backdrop.show();\n if (!this._config.scroll) {\n new ScrollBarHelper().hide();\n }\n this._element.setAttribute('aria-modal', true);\n this._element.setAttribute('role', 'dialog');\n this._element.classList.add(CLASS_NAME_SHOWING$1);\n const completeCallBack = () => {\n if (!this._config.scroll || this._config.backdrop) {\n this._focustrap.activate();\n }\n this._element.classList.add(CLASS_NAME_SHOW$3);\n this._element.classList.remove(CLASS_NAME_SHOWING$1);\n EventHandler.trigger(this._element, EVENT_SHOWN$3, {\n relatedTarget\n });\n };\n this._queueCallback(completeCallBack, this._element, true);\n }\n hide() {\n if (!this._isShown) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE$3);\n if (hideEvent.defaultPrevented) {\n return;\n }\n this._focustrap.deactivate();\n this._element.blur();\n this._isShown = false;\n this._element.classList.add(CLASS_NAME_HIDING);\n this._backdrop.hide();\n const completeCallback = () => {\n this._element.classList.remove(CLASS_NAME_SHOW$3, CLASS_NAME_HIDING);\n this._element.removeAttribute('aria-modal');\n this._element.removeAttribute('role');\n if (!this._config.scroll) {\n new ScrollBarHelper().reset();\n }\n EventHandler.trigger(this._element, EVENT_HIDDEN$3);\n };\n this._queueCallback(completeCallback, this._element, true);\n }\n dispose() {\n this._backdrop.dispose();\n this._focustrap.deactivate();\n super.dispose();\n }\n\n // Private\n _initializeBackDrop() {\n const clickCallback = () => {\n if (this._config.backdrop === 'static') {\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n return;\n }\n this.hide();\n };\n\n // 'static' option will be translated to true, and booleans will keep their value\n const isVisible = Boolean(this._config.backdrop);\n return new Backdrop({\n className: CLASS_NAME_BACKDROP,\n isVisible,\n isAnimated: true,\n rootElement: this._element.parentNode,\n clickCallback: isVisible ? clickCallback : null\n });\n }\n _initializeFocusTrap() {\n return new FocusTrap({\n trapElement: this._element\n });\n }\n _addEventListeners() {\n EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n if (event.key !== ESCAPE_KEY) {\n return;\n }\n if (this._config.keyboard) {\n this.hide();\n return;\n }\n EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);\n });\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Offcanvas.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config](this);\n });\n }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API$1, SELECTOR_DATA_TOGGLE$1, function (event) {\n const target = SelectorEngine.getElementFromSelector(this);\n if (['A', 'AREA'].includes(this.tagName)) {\n event.preventDefault();\n }\n if (isDisabled(this)) {\n return;\n }\n EventHandler.one(target, EVENT_HIDDEN$3, () => {\n // focus on trigger when it is closed\n if (isVisible(this)) {\n this.focus();\n }\n });\n\n // avoid conflict when clicking a toggler of an offcanvas, while another is open\n const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR);\n if (alreadyOpen && alreadyOpen !== target) {\n Offcanvas.getInstance(alreadyOpen).hide();\n }\n const data = Offcanvas.getOrCreateInstance(target);\n data.toggle(this);\n});\nEventHandler.on(window, EVENT_LOAD_DATA_API$2, () => {\n for (const selector of SelectorEngine.find(OPEN_SELECTOR)) {\n Offcanvas.getOrCreateInstance(selector).show();\n }\n});\nEventHandler.on(window, EVENT_RESIZE, () => {\n for (const element of SelectorEngine.find('[aria-modal][class*=show][class*=offcanvas-]')) {\n if (getComputedStyle(element).position !== 'fixed') {\n Offcanvas.getOrCreateInstance(element).hide();\n }\n }\n});\nenableDismissTrigger(Offcanvas);\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Offcanvas);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/sanitizer.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n// js-docs-start allow-list\nconst ARIA_ATTRIBUTE_PATTERN = /^aria-[\\w-]*$/i;\nconst DefaultAllowlist = {\n // Global attributes allowed on any supplied element below.\n '*': ['class', 'dir', 'id', 'lang', 'role', ARIA_ATTRIBUTE_PATTERN],\n a: ['target', 'href', 'title', 'rel'],\n area: [],\n b: [],\n br: [],\n col: [],\n code: [],\n div: [],\n em: [],\n hr: [],\n h1: [],\n h2: [],\n h3: [],\n h4: [],\n h5: [],\n h6: [],\n i: [],\n img: ['src', 'srcset', 'alt', 'title', 'width', 'height'],\n li: [],\n ol: [],\n p: [],\n pre: [],\n s: [],\n small: [],\n span: [],\n sub: [],\n sup: [],\n strong: [],\n u: [],\n ul: []\n};\n// js-docs-end allow-list\n\nconst uriAttributes = new Set(['background', 'cite', 'href', 'itemtype', 'longdesc', 'poster', 'src', 'xlink:href']);\n\n/**\n * A pattern that recognizes URLs that are safe wrt. XSS in URL navigation\n * contexts.\n *\n * Shout-out to Angular https://github.com/angular/angular/blob/15.2.8/packages/core/src/sanitization/url_sanitizer.ts#L38\n */\n// eslint-disable-next-line unicorn/better-regex\nconst SAFE_URL_PATTERN = /^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i;\nconst allowedAttribute = (attribute, allowedAttributeList) => {\n const attributeName = attribute.nodeName.toLowerCase();\n if (allowedAttributeList.includes(attributeName)) {\n if (uriAttributes.has(attributeName)) {\n return Boolean(SAFE_URL_PATTERN.test(attribute.nodeValue));\n }\n return true;\n }\n\n // Check if a regular expression validates the attribute.\n return allowedAttributeList.filter(attributeRegex => attributeRegex instanceof RegExp).some(regex => regex.test(attributeName));\n};\nfunction sanitizeHtml(unsafeHtml, allowList, sanitizeFunction) {\n if (!unsafeHtml.length) {\n return unsafeHtml;\n }\n if (sanitizeFunction && typeof sanitizeFunction === 'function') {\n return sanitizeFunction(unsafeHtml);\n }\n const domParser = new window.DOMParser();\n const createdDocument = domParser.parseFromString(unsafeHtml, 'text/html');\n const elements = [].concat(...createdDocument.body.querySelectorAll('*'));\n for (const element of elements) {\n const elementName = element.nodeName.toLowerCase();\n if (!Object.keys(allowList).includes(elementName)) {\n element.remove();\n continue;\n }\n const attributeList = [].concat(...element.attributes);\n const allowedAttributes = [].concat(allowList['*'] || [], allowList[elementName] || []);\n for (const attribute of attributeList) {\n if (!allowedAttribute(attribute, allowedAttributes)) {\n element.removeAttribute(attribute.nodeName);\n }\n }\n }\n return createdDocument.body.innerHTML;\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap util/template-factory.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$5 = 'TemplateFactory';\nconst Default$4 = {\n allowList: DefaultAllowlist,\n content: {},\n // { selector : text , selector2 : text2 , }\n extraClass: '',\n html: false,\n sanitize: true,\n sanitizeFn: null,\n template: '
'\n};\nconst DefaultType$4 = {\n allowList: 'object',\n content: 'object',\n extraClass: '(string|function)',\n html: 'boolean',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n template: 'string'\n};\nconst DefaultContentType = {\n entry: '(string|element|function|null)',\n selector: '(string|element)'\n};\n\n/**\n * Class definition\n */\n\nclass TemplateFactory extends Config {\n constructor(config) {\n super();\n this._config = this._getConfig(config);\n }\n\n // Getters\n static get Default() {\n return Default$4;\n }\n static get DefaultType() {\n return DefaultType$4;\n }\n static get NAME() {\n return NAME$5;\n }\n\n // Public\n getContent() {\n return Object.values(this._config.content).map(config => this._resolvePossibleFunction(config)).filter(Boolean);\n }\n hasContent() {\n return this.getContent().length > 0;\n }\n changeContent(content) {\n this._checkContent(content);\n this._config.content = {\n ...this._config.content,\n ...content\n };\n return this;\n }\n toHtml() {\n const templateWrapper = document.createElement('div');\n templateWrapper.innerHTML = this._maybeSanitize(this._config.template);\n for (const [selector, text] of Object.entries(this._config.content)) {\n this._setContent(templateWrapper, text, selector);\n }\n const template = templateWrapper.children[0];\n const extraClass = this._resolvePossibleFunction(this._config.extraClass);\n if (extraClass) {\n template.classList.add(...extraClass.split(' '));\n }\n return template;\n }\n\n // Private\n _typeCheckConfig(config) {\n super._typeCheckConfig(config);\n this._checkContent(config.content);\n }\n _checkContent(arg) {\n for (const [selector, content] of Object.entries(arg)) {\n super._typeCheckConfig({\n selector,\n entry: content\n }, DefaultContentType);\n }\n }\n _setContent(template, content, selector) {\n const templateElement = SelectorEngine.findOne(selector, template);\n if (!templateElement) {\n return;\n }\n content = this._resolvePossibleFunction(content);\n if (!content) {\n templateElement.remove();\n return;\n }\n if (isElement(content)) {\n this._putElementInTemplate(getElement(content), templateElement);\n return;\n }\n if (this._config.html) {\n templateElement.innerHTML = this._maybeSanitize(content);\n return;\n }\n templateElement.textContent = content;\n }\n _maybeSanitize(arg) {\n return this._config.sanitize ? sanitizeHtml(arg, this._config.allowList, this._config.sanitizeFn) : arg;\n }\n _resolvePossibleFunction(arg) {\n return execute(arg, [this]);\n }\n _putElementInTemplate(element, templateElement) {\n if (this._config.html) {\n templateElement.innerHTML = '';\n templateElement.append(element);\n return;\n }\n templateElement.textContent = element.textContent;\n }\n}\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$4 = 'tooltip';\nconst DISALLOWED_ATTRIBUTES = new Set(['sanitize', 'allowList', 'sanitizeFn']);\nconst CLASS_NAME_FADE$2 = 'fade';\nconst CLASS_NAME_MODAL = 'modal';\nconst CLASS_NAME_SHOW$2 = 'show';\nconst SELECTOR_TOOLTIP_INNER = '.tooltip-inner';\nconst SELECTOR_MODAL = `.${CLASS_NAME_MODAL}`;\nconst EVENT_MODAL_HIDE = 'hide.bs.modal';\nconst TRIGGER_HOVER = 'hover';\nconst TRIGGER_FOCUS = 'focus';\nconst TRIGGER_CLICK = 'click';\nconst TRIGGER_MANUAL = 'manual';\nconst EVENT_HIDE$2 = 'hide';\nconst EVENT_HIDDEN$2 = 'hidden';\nconst EVENT_SHOW$2 = 'show';\nconst EVENT_SHOWN$2 = 'shown';\nconst EVENT_INSERTED = 'inserted';\nconst EVENT_CLICK$1 = 'click';\nconst EVENT_FOCUSIN$1 = 'focusin';\nconst EVENT_FOCUSOUT$1 = 'focusout';\nconst EVENT_MOUSEENTER = 'mouseenter';\nconst EVENT_MOUSELEAVE = 'mouseleave';\nconst AttachmentMap = {\n AUTO: 'auto',\n TOP: 'top',\n RIGHT: isRTL() ? 'left' : 'right',\n BOTTOM: 'bottom',\n LEFT: isRTL() ? 'right' : 'left'\n};\nconst Default$3 = {\n allowList: DefaultAllowlist,\n animation: true,\n boundary: 'clippingParents',\n container: false,\n customClass: '',\n delay: 0,\n fallbackPlacements: ['top', 'right', 'bottom', 'left'],\n html: false,\n offset: [0, 6],\n placement: 'top',\n popperConfig: null,\n sanitize: true,\n sanitizeFn: null,\n selector: false,\n template: '
' + '
' + '
' + '
',\n title: '',\n trigger: 'hover focus'\n};\nconst DefaultType$3 = {\n allowList: 'object',\n animation: 'boolean',\n boundary: '(string|element)',\n container: '(string|element|boolean)',\n customClass: '(string|function)',\n delay: '(number|object)',\n fallbackPlacements: 'array',\n html: 'boolean',\n offset: '(array|string|function)',\n placement: '(string|function)',\n popperConfig: '(null|object|function)',\n sanitize: 'boolean',\n sanitizeFn: '(null|function)',\n selector: '(string|boolean)',\n template: 'string',\n title: '(string|element|function)',\n trigger: 'string'\n};\n\n/**\n * Class definition\n */\n\nclass Tooltip extends BaseComponent {\n constructor(element, config) {\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper (https://popper.js.org)');\n }\n super(element, config);\n\n // Private\n this._isEnabled = true;\n this._timeout = 0;\n this._isHovered = null;\n this._activeTrigger = {};\n this._popper = null;\n this._templateFactory = null;\n this._newContent = null;\n\n // Protected\n this.tip = null;\n this._setListeners();\n if (!this._config.selector) {\n this._fixTitle();\n }\n }\n\n // Getters\n static get Default() {\n return Default$3;\n }\n static get DefaultType() {\n return DefaultType$3;\n }\n static get NAME() {\n return NAME$4;\n }\n\n // Public\n enable() {\n this._isEnabled = true;\n }\n disable() {\n this._isEnabled = false;\n }\n toggleEnabled() {\n this._isEnabled = !this._isEnabled;\n }\n toggle() {\n if (!this._isEnabled) {\n return;\n }\n this._activeTrigger.click = !this._activeTrigger.click;\n if (this._isShown()) {\n this._leave();\n return;\n }\n this._enter();\n }\n dispose() {\n clearTimeout(this._timeout);\n EventHandler.off(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n if (this._element.getAttribute('data-bs-original-title')) {\n this._element.setAttribute('title', this._element.getAttribute('data-bs-original-title'));\n }\n this._disposePopper();\n super.dispose();\n }\n show() {\n if (this._element.style.display === 'none') {\n throw new Error('Please use show on visible elements');\n }\n if (!(this._isWithContent() && this._isEnabled)) {\n return;\n }\n const showEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOW$2));\n const shadowRoot = findShadowRoot(this._element);\n const isInTheDom = (shadowRoot || this._element.ownerDocument.documentElement).contains(this._element);\n if (showEvent.defaultPrevented || !isInTheDom) {\n return;\n }\n\n // TODO: v6 remove this or make it optional\n this._disposePopper();\n const tip = this._getTipElement();\n this._element.setAttribute('aria-describedby', tip.getAttribute('id'));\n const {\n container\n } = this._config;\n if (!this._element.ownerDocument.documentElement.contains(this.tip)) {\n container.append(tip);\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED));\n }\n this._popper = this._createPopper(tip);\n tip.classList.add(CLASS_NAME_SHOW$2);\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.on(element, 'mouseover', noop);\n }\n }\n const complete = () => {\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_SHOWN$2));\n if (this._isHovered === false) {\n this._leave();\n }\n this._isHovered = false;\n };\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n hide() {\n if (!this._isShown()) {\n return;\n }\n const hideEvent = EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDE$2));\n if (hideEvent.defaultPrevented) {\n return;\n }\n const tip = this._getTipElement();\n tip.classList.remove(CLASS_NAME_SHOW$2);\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n for (const element of [].concat(...document.body.children)) {\n EventHandler.off(element, 'mouseover', noop);\n }\n }\n this._activeTrigger[TRIGGER_CLICK] = false;\n this._activeTrigger[TRIGGER_FOCUS] = false;\n this._activeTrigger[TRIGGER_HOVER] = false;\n this._isHovered = null; // it is a trick to support manual triggering\n\n const complete = () => {\n if (this._isWithActiveTrigger()) {\n return;\n }\n if (!this._isHovered) {\n this._disposePopper();\n }\n this._element.removeAttribute('aria-describedby');\n EventHandler.trigger(this._element, this.constructor.eventName(EVENT_HIDDEN$2));\n };\n this._queueCallback(complete, this.tip, this._isAnimated());\n }\n update() {\n if (this._popper) {\n this._popper.update();\n }\n }\n\n // Protected\n _isWithContent() {\n return Boolean(this._getTitle());\n }\n _getTipElement() {\n if (!this.tip) {\n this.tip = this._createTipElement(this._newContent || this._getContentForTemplate());\n }\n return this.tip;\n }\n _createTipElement(content) {\n const tip = this._getTemplateFactory(content).toHtml();\n\n // TODO: remove this check in v6\n if (!tip) {\n return null;\n }\n tip.classList.remove(CLASS_NAME_FADE$2, CLASS_NAME_SHOW$2);\n // TODO: v6 the following can be achieved with CSS only\n tip.classList.add(`bs-${this.constructor.NAME}-auto`);\n const tipId = getUID(this.constructor.NAME).toString();\n tip.setAttribute('id', tipId);\n if (this._isAnimated()) {\n tip.classList.add(CLASS_NAME_FADE$2);\n }\n return tip;\n }\n setContent(content) {\n this._newContent = content;\n if (this._isShown()) {\n this._disposePopper();\n this.show();\n }\n }\n _getTemplateFactory(content) {\n if (this._templateFactory) {\n this._templateFactory.changeContent(content);\n } else {\n this._templateFactory = new TemplateFactory({\n ...this._config,\n // the `content` var has to be after `this._config`\n // to override config.content in case of popover\n content,\n extraClass: this._resolvePossibleFunction(this._config.customClass)\n });\n }\n return this._templateFactory;\n }\n _getContentForTemplate() {\n return {\n [SELECTOR_TOOLTIP_INNER]: this._getTitle()\n };\n }\n _getTitle() {\n return this._resolvePossibleFunction(this._config.title) || this._element.getAttribute('data-bs-original-title');\n }\n\n // Private\n _initializeOnDelegatedTarget(event) {\n return this.constructor.getOrCreateInstance(event.delegateTarget, this._getDelegateConfig());\n }\n _isAnimated() {\n return this._config.animation || this.tip && this.tip.classList.contains(CLASS_NAME_FADE$2);\n }\n _isShown() {\n return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW$2);\n }\n _createPopper(tip) {\n const placement = execute(this._config.placement, [this, tip, this._element]);\n const attachment = AttachmentMap[placement.toUpperCase()];\n return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment));\n }\n _getOffset() {\n const {\n offset\n } = this._config;\n if (typeof offset === 'string') {\n return offset.split(',').map(value => Number.parseInt(value, 10));\n }\n if (typeof offset === 'function') {\n return popperData => offset(popperData, this._element);\n }\n return offset;\n }\n _resolvePossibleFunction(arg) {\n return execute(arg, [this._element]);\n }\n _getPopperConfig(attachment) {\n const defaultBsPopperConfig = {\n placement: attachment,\n modifiers: [{\n name: 'flip',\n options: {\n fallbackPlacements: this._config.fallbackPlacements\n }\n }, {\n name: 'offset',\n options: {\n offset: this._getOffset()\n }\n }, {\n name: 'preventOverflow',\n options: {\n boundary: this._config.boundary\n }\n }, {\n name: 'arrow',\n options: {\n element: `.${this.constructor.NAME}-arrow`\n }\n }, {\n name: 'preSetPlacement',\n enabled: true,\n phase: 'beforeMain',\n fn: data => {\n // Pre-set Popper's placement attribute in order to read the arrow sizes properly.\n // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement\n this._getTipElement().setAttribute('data-popper-placement', data.state.placement);\n }\n }]\n };\n return {\n ...defaultBsPopperConfig,\n ...execute(this._config.popperConfig, [defaultBsPopperConfig])\n };\n }\n _setListeners() {\n const triggers = this._config.trigger.split(' ');\n for (const trigger of triggers) {\n if (trigger === 'click') {\n EventHandler.on(this._element, this.constructor.eventName(EVENT_CLICK$1), this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context.toggle();\n });\n } else if (trigger !== TRIGGER_MANUAL) {\n const eventIn = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSEENTER) : this.constructor.eventName(EVENT_FOCUSIN$1);\n const eventOut = trigger === TRIGGER_HOVER ? this.constructor.eventName(EVENT_MOUSELEAVE) : this.constructor.eventName(EVENT_FOCUSOUT$1);\n EventHandler.on(this._element, eventIn, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context._activeTrigger[event.type === 'focusin' ? TRIGGER_FOCUS : TRIGGER_HOVER] = true;\n context._enter();\n });\n EventHandler.on(this._element, eventOut, this._config.selector, event => {\n const context = this._initializeOnDelegatedTarget(event);\n context._activeTrigger[event.type === 'focusout' ? TRIGGER_FOCUS : TRIGGER_HOVER] = context._element.contains(event.relatedTarget);\n context._leave();\n });\n }\n }\n this._hideModalHandler = () => {\n if (this._element) {\n this.hide();\n }\n };\n EventHandler.on(this._element.closest(SELECTOR_MODAL), EVENT_MODAL_HIDE, this._hideModalHandler);\n }\n _fixTitle() {\n const title = this._element.getAttribute('title');\n if (!title) {\n return;\n }\n if (!this._element.getAttribute('aria-label') && !this._element.textContent.trim()) {\n this._element.setAttribute('aria-label', title);\n }\n this._element.setAttribute('data-bs-original-title', title); // DO NOT USE IT. Is only for backwards compatibility\n this._element.removeAttribute('title');\n }\n _enter() {\n if (this._isShown() || this._isHovered) {\n this._isHovered = true;\n return;\n }\n this._isHovered = true;\n this._setTimeout(() => {\n if (this._isHovered) {\n this.show();\n }\n }, this._config.delay.show);\n }\n _leave() {\n if (this._isWithActiveTrigger()) {\n return;\n }\n this._isHovered = false;\n this._setTimeout(() => {\n if (!this._isHovered) {\n this.hide();\n }\n }, this._config.delay.hide);\n }\n _setTimeout(handler, timeout) {\n clearTimeout(this._timeout);\n this._timeout = setTimeout(handler, timeout);\n }\n _isWithActiveTrigger() {\n return Object.values(this._activeTrigger).includes(true);\n }\n _getConfig(config) {\n const dataAttributes = Manipulator.getDataAttributes(this._element);\n for (const dataAttribute of Object.keys(dataAttributes)) {\n if (DISALLOWED_ATTRIBUTES.has(dataAttribute)) {\n delete dataAttributes[dataAttribute];\n }\n }\n config = {\n ...dataAttributes,\n ...(typeof config === 'object' && config ? config : {})\n };\n config = this._mergeConfigObj(config);\n config = this._configAfterMerge(config);\n this._typeCheckConfig(config);\n return config;\n }\n _configAfterMerge(config) {\n config.container = config.container === false ? document.body : getElement(config.container);\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n };\n }\n if (typeof config.title === 'number') {\n config.title = config.title.toString();\n }\n if (typeof config.content === 'number') {\n config.content = config.content.toString();\n }\n return config;\n }\n _getDelegateConfig() {\n const config = {};\n for (const [key, value] of Object.entries(this._config)) {\n if (this.constructor.Default[key] !== value) {\n config[key] = value;\n }\n }\n config.selector = false;\n config.trigger = 'manual';\n\n // In the future can be replaced with:\n // const keysWithDifferentValues = Object.entries(this._config).filter(entry => this.constructor.Default[entry[0]] !== this._config[entry[0]])\n // `Object.fromEntries(keysWithDifferentValues)`\n return config;\n }\n _disposePopper() {\n if (this._popper) {\n this._popper.destroy();\n this._popper = null;\n }\n if (this.tip) {\n this.tip.remove();\n this.tip = null;\n }\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Tooltip.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Tooltip);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$3 = 'popover';\nconst SELECTOR_TITLE = '.popover-header';\nconst SELECTOR_CONTENT = '.popover-body';\nconst Default$2 = {\n ...Tooltip.Default,\n content: '',\n offset: [0, 8],\n placement: 'right',\n template: '
' + '
' + '

' + '
' + '
',\n trigger: 'click'\n};\nconst DefaultType$2 = {\n ...Tooltip.DefaultType,\n content: '(null|string|element|function)'\n};\n\n/**\n * Class definition\n */\n\nclass Popover extends Tooltip {\n // Getters\n static get Default() {\n return Default$2;\n }\n static get DefaultType() {\n return DefaultType$2;\n }\n static get NAME() {\n return NAME$3;\n }\n\n // Overrides\n _isWithContent() {\n return this._getTitle() || this._getContent();\n }\n\n // Private\n _getContentForTemplate() {\n return {\n [SELECTOR_TITLE]: this._getTitle(),\n [SELECTOR_CONTENT]: this._getContent()\n };\n }\n _getContent() {\n return this._resolvePossibleFunction(this._config.content);\n }\n\n // Static\n static jQueryInterface(config) {\n return this.each(function () {\n const data = Popover.getOrCreateInstance(this, config);\n if (typeof config !== 'string') {\n return;\n }\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`);\n }\n data[config]();\n });\n }\n}\n\n/**\n * jQuery\n */\n\ndefineJQueryPlugin(Popover);\n\n/**\n * --------------------------------------------------------------------------\n * Bootstrap scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\n\n/**\n * Constants\n */\n\nconst NAME$2 = 'scrollspy';\nconst DATA_KEY$2 = 'bs.scrollspy';\nconst EVENT_KEY$2 = `.${DATA_KEY$2}`;\nconst DATA_API_KEY = '.data-api';\nconst EVENT_ACTIVATE = `activate${EVENT_KEY$2}`;\nconst EVENT_CLICK = `click${EVENT_KEY$2}`;\nconst EVENT_LOAD_DATA_API$1 = `load${EVENT_KEY$2}${DATA_API_KEY}`;\nconst CLASS_NAME_DROPDOWN_ITEM = 'dropdown-item';\nconst CLASS_NAME_ACTIVE$1 = 'active';\nconst SELECTOR_DATA_SPY = '[data-bs-spy=\"scroll\"]';\nconst SELECTOR_TARGET_LINKS = '[href]';\nconst SELECTOR_NAV_LIST_GROUP = '.nav, .list-group';\nconst SELECTOR_NAV_LINKS = '.nav-link';\nconst SELECTOR_NAV_ITEMS = '.nav-item';\nconst SELECTOR_LIST_ITEMS = '.list-group-item';\nconst SELECTOR_LINK_ITEMS = `${SELECTOR_NAV_LINKS}, ${SELECTOR_NAV_ITEMS} > ${SELECTOR_NAV_LINKS}, ${SELECTOR_LIST_ITEMS}`;\nconst SELECTOR_DROPDOWN = '.dropdown';\nconst SELECTOR_DROPDOWN_TOGGLE$1 = '.dropdown-toggle';\nconst Default$1 = {\n offset: null,\n // TODO: v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: '0px 0px -25%',\n smoothScroll: false,\n target: null,\n threshold: [0.1, 0.5, 1]\n};\nconst DefaultType$1 = {\n offset: '(number|null)',\n // TODO v6 @deprecated, keep it for backwards compatibility reasons\n rootMargin: 'string',\n smoothScroll: 'boolean',\n target: 'element',\n threshold: 'array'\n};\n\n/**\n * Class definition\n */\n\nclass ScrollSpy extends BaseComponent {\n constructor(element, config) {\n super(element, config);\n\n // this._element is the observablesContainer and config.target the menu links wrapper\n this._targetLinks = new Map();\n this._observableSections = new Map();\n this._rootElement = getComputedStyle(this._element).overflowY === 'visible' ? null : this._element;\n this._activeTarget = null;\n this._observer = null;\n this._previousScrollData = {\n visibleEntryTop: 0,\n parentScrollTop: 0\n };\n this.refresh(); // initialize\n }\n\n // Getters\n static get Default() {\n return Default$1;\n }\n static get DefaultType() {\n return DefaultType$1;\n }\n static get NAME() {\n return NAME$2;\n }\n\n // Public\n refresh() {\n this._initializeTargetsAndObservables();\n this._maybeEnableSmoothScroll();\n if (this._observer) {\n this._observer.disconnect();\n } else {\n this._observer = this._getNewObserver();\n }\n for (const section of this._observableSections.values()) {\n this._observer.observe(section);\n }\n }\n dispose() {\n this._observer.disconnect();\n super.dispose();\n }\n\n // Private\n _configAfterMerge(config) {\n // TODO: on v6 target should be given explicitly & remove the {target: 'ss-target'} case\n config.target = getElement(config.target) || document.body;\n\n // TODO: v6 Only for backwards compatibility reasons. Use rootMargin only\n config.rootMargin = config.offset ? `${config.offset}px 0px -30%` : config.rootMargin;\n if (typeof config.threshold === 'string') {\n config.threshold = config.threshold.split(',').map(value => Number.parseFloat(value));\n }\n return config;\n }\n _maybeEnableSmoothScroll() {\n if (!this._config.smoothScroll) {\n return;\n }\n\n // unregister any previous listeners\n EventHandler.off(this._config.target, EVENT_CLICK);\n EventHandler.on(this._config.target, EVENT_CLICK, SELECTOR_TARGET_LINKS, event => {\n const observableSection = this._observableSections.get(event.target.hash);\n if (observableSection) {\n event.preventDefault();\n const root = this._rootElement || window;\n const height = observableSection.offsetTop - this._element.offsetTop;\n if (root.scrollTo) {\n root.scrollTo({\n top: height,\n behavior: 'smooth'\n });\n return;\n }\n\n // Chrome 60 doesn't support `scrollTo`\n root.scrollTop = height;\n }\n });\n }\n _getNewObserver() {\n const options = {\n root: this._rootElement,\n threshold: this._config.threshold,\n rootMargin: this._config.rootMargin\n };\n return new IntersectionObserver(entries => this._observerCallback(entries), options);\n }\n\n // The logic of selection\n _observerCallback(entries) {\n const targetElement = entry => this._targetLinks.get(`#${entry.target.id}`);\n const activate = entry => {\n this._previousScrollData.visibleEntryTop = entry.target.offsetTop;\n this._process(targetElement(entry));\n };\n const parentScrollTop = (this._rootElement || document.documentElement).scrollTop;\n const userScrollsDown = parentScrollTop >= this._previousScrollData.parentScrollTop;\n this._previousScrollData.parentScrollTop = parentScrollTop;\n for (const entry of entries) {\n if (!entry.isIntersecting) {\n this._activeTarget = null;\n this._clearActiveClass(targetElement(entry));\n continue;\n }\n const entryIsLowerThanPrevious = entry.target.offsetTop >= this._previousScrollData.visibleEntryTop;\n // if we are scrolling down, pick the bigger offsetTop\n if (userScrollsDown && entryIsLowerThanPrevious) {\n activate(entry);\n // if parent isn't scrolled, let's keep the first visible item, breaking the iteration\n if (!parentScrollTop) {\n return;\n }\n continue;\n }\n\n // if we are scrolling up, pick the smallest offsetTop\n if (!userScrollsDown && !entryIsLowerThanPrevious) {\n activate(entry);\n }\n }\n }\n _initializeTargetsAndObservables() {\n this._targetLinks = new Map();\n this._observableSections = new Map();\n const targetLinks = SelectorEngine.find(SELECTOR_TARGET_LINKS, this._config.target);\n for (const anchor of targetLinks) {\n // ensure that the anchor has an id and is not disabled\n if (!anchor.hash || isDisabled(anchor)) {\n continue;\n }\n const observableSection = SelectorEngine.findOne(decodeURI(anchor.hash), this._element);\n\n // ensure that the observableSection exists & is visible\n if (isVisible(observableSection)) {\n this._targetLinks.set(decodeURI(anchor.hash), anchor);\n this._observableSections.set(anchor.hash, observableSection);\n }\n }\n }\n _process(target) {\n if (this._activeTarget === target) {\n return;\n }\n this._clearActiveClass(this._config.target);\n this._activeTarget = target;\n target.classList.add(CLASS_NAME_ACTIVE$1);\n this._activateParents(target);\n EventHandler.trigger(this._element, EVENT_ACTIVATE, {\n relatedTarget: target\n });\n }\n _activateParents(target) {\n // Activate dropdown parents\n if (target.classList.contains(CLASS_NAME_DROPDOWN_ITEM)) {\n SelectorEngine.findOne(SELECTOR_DROPDOWN_TOGGLE$1, target.closest(SELECTOR_DROPDOWN)).classList.add(CLASS_NAME_ACTIVE$1);\n return;\n }\n for (const listGroup of SelectorEngine.parents(target, SELECTOR_NAV_LIST_GROUP)) {\n // Set triggered links parents as active\n // With both