text:
# This file is auto-generated by AWSMetadata.jl
using AWS
using AWS.AWSServices: personalize
using AWS.Compat
using AWS.UUIDs

"""
    CreateBatchInferenceJob()

Creates a batch inference job. The operation can handle up to 50 million records and the input file must be in JSON format. For more information, see recommendations-batch.

# Required Parameters
- `jobInput`: The Amazon S3 path that leads to the input file to base your recommendations on. The input material must be in JSON format.
- `jobName`: The name of the batch inference job to create.
- `jobOutput`: The path to the Amazon S3 bucket where the job's output will be stored.
- `roleArn`: The ARN of the Amazon Identity and Access Management role that has permissions to read and write to your input and out Amazon S3 buckets respectively.
- `solutionVersionArn`: The Amazon Resource Name (ARN) of the solution version that will be used to generate the batch inference recommendations.

# Optional Parameters
- `batchInferenceJobConfig`: The configuration details of a batch inference job.
- `filterArn`: The ARN of the filter to apply to the batch inference job. For more information on using filters, see Using Filters with Amazon Personalize.
- `numResults`: The number of recommendations to retreive.
"""
create_batch_inference_job(jobInput, jobName, jobOutput, roleArn, solutionVersionArn; aws_config::AWSConfig=global_aws_config()) = personalize("CreateBatchInferenceJob", Dict{String, Any}("jobInput"=>jobInput, "jobName"=>jobName, "jobOutput"=>jobOutput, "roleArn"=>roleArn, "solutionVersionArn"=>solutionVersionArn); aws_config=aws_config)
create_batch_inference_job(jobInput, jobName, jobOutput, roleArn, solutionVersionArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("CreateBatchInferenceJob", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("jobInput"=>jobInput, "jobName"=>jobName, "jobOutput"=>jobOutput, "roleArn"=>roleArn, "solutionVersionArn"=>solutionVersionArn), args)); aws_config=aws_config)

"""
    CreateCampaign()

Creates a campaign by deploying a solution version. When a client calls the GetRecommendations and GetPersonalizedRanking APIs, a campaign is specified in the request.  Minimum Provisioned TPS and Auto-Scaling  A transaction is a single GetRecommendations or GetPersonalizedRanking call. Transactions per second (TPS) is the throughput and unit of billing for Amazon Personalize. The minimum provisioned TPS (minProvisionedTPS) specifies the baseline throughput provisioned by Amazon Personalize, and thus, the minimum billing charge. If your TPS increases beyond minProvisionedTPS, Amazon Personalize auto-scales the provisioned capacity up and down, but never below minProvisionedTPS, to maintain a 70% utilization. There's a short time delay while the capacity is increased that might cause loss of transactions. It's recommended to start with a low minProvisionedTPS, track your usage using Amazon CloudWatch metrics, and then increase the minProvisionedTPS as necessary.  Status  A campaign can be in one of the following states:   CREATE PENDING > CREATE IN_PROGRESS > ACTIVE -or- CREATE FAILED   DELETE PENDING > DELETE IN_PROGRESS   To get the campaign status, call DescribeCampaign.  Wait until the status of the campaign is ACTIVE before asking the campaign for recommendations.   Related APIs     ListCampaigns     DescribeCampaign     UpdateCampaign     DeleteCampaign

# Required Parameters
- `minProvisionedTPS`: Specifies the requested minimum provisioned transactions (recommendations) per second that Amazon Personalize will support.
- `name`: A name for the new campaign. The campaign name must be unique within your account.
- `solutionVersionArn`: The Amazon Resource Name (ARN) of the solution version to deploy.

# Optional Parameters
- `campaignConfig`: The configuration details of a campaign.
"""
create_campaign(minProvisionedTPS, name, solutionVersionArn; aws_config::AWSConfig=global_aws_config()) = personalize("CreateCampaign", Dict{String, Any}("minProvisionedTPS"=>minProvisionedTPS, "name"=>name, "solutionVersionArn"=>solutionVersionArn); aws_config=aws_config)
create_campaign(minProvisionedTPS, name, solutionVersionArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("CreateCampaign", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("minProvisionedTPS"=>minProvisionedTPS, "name"=>name, "solutionVersionArn"=>solutionVersionArn), args)); aws_config=aws_config)

"""
    CreateDataset()

Creates an empty dataset and adds it to the specified dataset group. Use CreateDatasetImportJob to import your training data to a dataset. There are three types of datasets:   Interactions   Items   Users   Each dataset type has an associated schema with required field types. Only the Interactions dataset is required in order to train a model (also referred to as creating a solution). A dataset can be in one of the following states:   CREATE PENDING > CREATE IN_PROGRESS > ACTIVE -or- CREATE FAILED   DELETE PENDING > DELETE IN_PROGRESS   To get the status of the dataset, call DescribeDataset.  Related APIs     CreateDatasetGroup     ListDatasets     DescribeDataset     DeleteDataset

# Required Parameters
- `datasetGroupArn`: The Amazon Resource Name (ARN) of the dataset group to add the dataset to.
- `datasetType`: The type of dataset. One of the following (case insensitive) values:   Interactions   Items   Users
- `name`: The name for the dataset.
- `schemaArn`: The ARN of the schema to associate with the dataset. The schema defines the dataset fields.

"""
create_dataset(datasetGroupArn, datasetType, name, schemaArn; aws_config::AWSConfig=global_aws_config()) = personalize("CreateDataset", Dict{String, Any}("datasetGroupArn"=>datasetGroupArn, "datasetType"=>datasetType, "name"=>name, "schemaArn"=>schemaArn); aws_config=aws_config)
create_dataset(datasetGroupArn, datasetType, name, schemaArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("CreateDataset", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("datasetGroupArn"=>datasetGroupArn, "datasetType"=>datasetType, "name"=>name, "schemaArn"=>schemaArn), args)); aws_config=aws_config)

"""
    CreateDatasetGroup()

Creates an empty dataset group. A dataset group contains related datasets that supply data for training a model. A dataset group can contain at most three datasets, one for each type of dataset:   Interactions   Items   Users   To train a model (create a solution), a dataset group that contains an Interactions dataset is required. Call CreateDataset to add a dataset to the group. A dataset group can be in one of the following states:   CREATE PENDING > CREATE IN_PROGRESS > ACTIVE -or- CREATE FAILED   DELETE PENDING   To get the status of the dataset group, call DescribeDatasetGroup. If the status shows as CREATE FAILED, the response includes a failureReason key, which describes why the creation failed.  You must wait until the status of the dataset group is ACTIVE before adding a dataset to the group.  You can specify an AWS Key Management Service (KMS) key to encrypt the datasets in the group. If you specify a KMS key, you must also include an AWS Identity and Access Management (IAM) role that has permission to access the key.  APIs that require a dataset group ARN in the request     CreateDataset     CreateEventTracker     CreateSolution     Related APIs     ListDatasetGroups     DescribeDatasetGroup     DeleteDatasetGroup

# Required Parameters
- `name`: The name for the new dataset group.

# Optional Parameters
- `kmsKeyArn`: The Amazon Resource Name (ARN) of a KMS key used to encrypt the datasets.
- `roleArn`: The ARN of the IAM role that has permissions to access the KMS key. Supplying an IAM role is only valid when also specifying a KMS key.
"""
create_dataset_group(name; aws_config::AWSConfig=global_aws_config()) = personalize("CreateDatasetGroup", Dict{String, Any}("name"=>name); aws_config=aws_config)
create_dataset_group(name, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("CreateDatasetGroup", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("name"=>name), args)); aws_config=aws_config)

"""
    CreateDatasetImportJob()

Creates a job that imports training data from your data source (an Amazon S3 bucket) to an Amazon Personalize dataset. To allow Amazon Personalize to import the training data, you must specify an AWS Identity and Access Management (IAM) role that has permission to read from the data source, as Amazon Personalize makes a copy of your data and processes it in an internal AWS system.  The dataset import job replaces any previous data in the dataset.   Status  A dataset import job can be in one of the following states:   CREATE PENDING > CREATE IN_PROGRESS > ACTIVE -or- CREATE FAILED   To get the status of the import job, call DescribeDatasetImportJob, providing the Amazon Resource Name (ARN) of the dataset import job. The dataset import is complete when the status shows as ACTIVE. If the status shows as CREATE FAILED, the response includes a failureReason key, which describes why the job failed.  Importing takes time. You must wait until the status shows as ACTIVE before training a model using the dataset.   Related APIs     ListDatasetImportJobs     DescribeDatasetImportJob

# Required Parameters
- `dataSource`: The Amazon S3 bucket that contains the training data to import.
- `datasetArn`: The ARN of the dataset that receives the imported data.
- `jobName`: The name for the dataset import job.
- `roleArn`: The ARN of the IAM role that has permissions to read from the Amazon S3 data source.

"""
create_dataset_import_job(dataSource, datasetArn, jobName, roleArn; aws_config::AWSConfig=global_aws_config()) = personalize("CreateDatasetImportJob", Dict{String, Any}("dataSource"=>dataSource, "datasetArn"=>datasetArn, "jobName"=>jobName, "roleArn"=>roleArn); aws_config=aws_config)
create_dataset_import_job(dataSource, datasetArn, jobName, roleArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("CreateDatasetImportJob", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("dataSource"=>dataSource, "datasetArn"=>datasetArn, "jobName"=>jobName, "roleArn"=>roleArn), args)); aws_config=aws_config)

"""
    CreateEventTracker()

Creates an event tracker that you use when sending event data to the specified dataset group using the PutEvents API. When Amazon Personalize creates an event tracker, it also creates an event-interactions dataset in the dataset group associated with the event tracker. The event-interactions dataset stores the event data from the PutEvents call. The contents of this dataset are not available to the user.  Only one event tracker can be associated with a dataset group. You will get an error if you call CreateEventTracker using the same dataset group as an existing event tracker.  When you send event data you include your tracking ID. The tracking ID identifies the customer and authorizes the customer to send the data. The event tracker can be in one of the following states:   CREATE PENDING > CREATE IN_PROGRESS > ACTIVE -or- CREATE FAILED   DELETE PENDING > DELETE IN_PROGRESS   To get the status of the event tracker, call DescribeEventTracker.  The event tracker must be in the ACTIVE state before using the tracking ID.   Related APIs     ListEventTrackers     DescribeEventTracker     DeleteEventTracker

# Required Parameters
- `datasetGroupArn`: The Amazon Resource Name (ARN) of the dataset group that receives the event data.
- `name`: The name for the event tracker.

"""
create_event_tracker(datasetGroupArn, name; aws_config::AWSConfig=global_aws_config()) = personalize("CreateEventTracker", Dict{String, Any}("datasetGroupArn"=>datasetGroupArn, "name"=>name); aws_config=aws_config)
create_event_tracker(datasetGroupArn, name, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("CreateEventTracker", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("datasetGroupArn"=>datasetGroupArn, "name"=>name), args)); aws_config=aws_config)

"""
    CreateFilter()

Creates a recommendation filter. For more information, see Using Filters with Amazon Personalize.

# Required Parameters
- `datasetGroupArn`: The ARN of the dataset group that the filter will belong to.
- `filterExpression`: The filter expression that designates the interaction types that the filter will filter out. A filter expression must follow the following format:  EXCLUDE itemId WHERE INTERACTIONS.event_type in (\"EVENT_TYPE\")  Where \"EVENT_TYPE\" is the type of event to filter out. To filter out all items with any interactions history, set \"*\" as the EVENT_TYPE. For more information, see Using Filters with Amazon Personalize.
- `name`: The name of the filter to create.

"""
create_filter(datasetGroupArn, filterExpression, name; aws_config::AWSConfig=global_aws_config()) = personalize("CreateFilter", Dict{String, Any}("datasetGroupArn"=>datasetGroupArn, "filterExpression"=>filterExpression, "name"=>name); aws_config=aws_config)
create_filter(datasetGroupArn, filterExpression, name, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("CreateFilter", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("datasetGroupArn"=>datasetGroupArn, "filterExpression"=>filterExpression, "name"=>name), args)); aws_config=aws_config)

"""
    CreateSchema()

Creates an Amazon Personalize schema from the specified schema string. The schema you create must be in Avro JSON format. Amazon Personalize recognizes three schema variants. Each schema is associated with a dataset type and has a set of required field and keywords. You specify a schema when you call CreateDataset.  Related APIs     ListSchemas     DescribeSchema     DeleteSchema

# Required Parameters
- `name`: The name for the schema.
- `schema`: A schema in Avro JSON format.

"""
create_schema(name, schema; aws_config::AWSConfig=global_aws_config()) = personalize("CreateSchema", Dict{String, Any}("name"=>name, "schema"=>schema); aws_config=aws_config)
create_schema(name, schema, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("CreateSchema", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("name"=>name, "schema"=>schema), args)); aws_config=aws_config)

"""
    CreateSolution()

Creates the configuration for training a model. A trained model is known as a solution. After the configuration is created, you train the model (create a solution) by calling the CreateSolutionVersion operation. Every time you call CreateSolutionVersion, a new version of the solution is created. After creating a solution version, you check its accuracy by calling GetSolutionMetrics. When you are satisfied with the version, you deploy it using CreateCampaign. The campaign provides recommendations to a client through the GetRecommendations API. To train a model, Amazon Personalize requires training data and a recipe. The training data comes from the dataset group that you provide in the request. A recipe specifies the training algorithm and a feature transformation. You can specify one of the predefined recipes provided by Amazon Personalize. Alternatively, you can specify performAutoML and Amazon Personalize will analyze your data and select the optimum USER_PERSONALIZATION recipe for you.  Status  A solution can be in one of the following states:   CREATE PENDING > CREATE IN_PROGRESS > ACTIVE -or- CREATE FAILED   DELETE PENDING > DELETE IN_PROGRESS   To get the status of the solution, call DescribeSolution. Wait until the status shows as ACTIVE before calling CreateSolutionVersion.  Related APIs     ListSolutions     CreateSolutionVersion     DescribeSolution     DeleteSolution       ListSolutionVersions     DescribeSolutionVersion

# Required Parameters
- `datasetGroupArn`: The Amazon Resource Name (ARN) of the dataset group that provides the training data.
- `name`: The name for the solution.

# Optional Parameters
- `eventType`: When your have multiple event types (using an EVENT_TYPE schema field), this parameter specifies which event type (for example, 'click' or 'like') is used for training the model.
- `performAutoML`: Whether to perform automated machine learning (AutoML). The default is false. For this case, you must specify recipeArn. When set to true, Amazon Personalize analyzes your training data and selects the optimal USER_PERSONALIZATION recipe and hyperparameters. In this case, you must omit recipeArn. Amazon Personalize determines the optimal recipe by running tests with different values for the hyperparameters. AutoML lengthens the training process as compared to selecting a specific recipe.
- `performHPO`: Whether to perform hyperparameter optimization (HPO) on the specified or selected recipe. The default is false. When performing AutoML, this parameter is always true and you should not set it to false.
- `recipeArn`: The ARN of the recipe to use for model training. Only specified when performAutoML is false.
- `solutionConfig`: The configuration to use with the solution. When performAutoML is set to true, Amazon Personalize only evaluates the autoMLConfig section of the solution configuration.
"""
create_solution(datasetGroupArn, name; aws_config::AWSConfig=global_aws_config()) = personalize("CreateSolution", Dict{String, Any}("datasetGroupArn"=>datasetGroupArn, "name"=>name); aws_config=aws_config)
create_solution(datasetGroupArn, name, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("CreateSolution", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("datasetGroupArn"=>datasetGroupArn, "name"=>name), args)); aws_config=aws_config)

"""
    CreateSolutionVersion()

Trains or retrains an active solution. A solution is created using the CreateSolution operation and must be in the ACTIVE state before calling CreateSolutionVersion. A new version of the solution is created every time you call this operation.  Status  A solution version can be in one of the following states:   CREATE PENDING > CREATE IN_PROGRESS > ACTIVE -or- CREATE FAILED   To get the status of the version, call DescribeSolutionVersion. Wait until the status shows as ACTIVE before calling CreateCampaign. If the status shows as CREATE FAILED, the response includes a failureReason key, which describes why the job failed.  Related APIs     ListSolutionVersions     DescribeSolutionVersion       ListSolutions     CreateSolution     DescribeSolution     DeleteSolution

# Required Parameters
- `solutionArn`: The Amazon Resource Name (ARN) of the solution containing the training configuration information.

# Optional Parameters
- `trainingMode`: The scope of training to be performed when creating the solution version. The FULL option trains the solution version based on the entirety of the input solution's training data, while the UPDATE option processes only the data that has changed in comparison to the input solution. Choose UPDATE when you want to incrementally update your solution version instead of creating an entirely new one.  The UPDATE option can only be used when you already have an active solution version created from the input solution using the FULL option and the input solution was trained with the native-recipe-hrnn-coldstart recipe.
"""
create_solution_version(solutionArn; aws_config::AWSConfig=global_aws_config()) = personalize("CreateSolutionVersion", Dict{String, Any}("solutionArn"=>solutionArn); aws_config=aws_config)
create_solution_version(solutionArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("CreateSolutionVersion", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("solutionArn"=>solutionArn), args)); aws_config=aws_config)

"""
    DeleteCampaign()

Removes a campaign by deleting the solution deployment. The solution that the campaign is based on is not deleted and can be redeployed when needed. A deleted campaign can no longer be specified in a GetRecommendations request. For more information on campaigns, see CreateCampaign.

# Required Parameters
- `campaignArn`: The Amazon Resource Name (ARN) of the campaign to delete.

"""
delete_campaign(campaignArn; aws_config::AWSConfig=global_aws_config()) = personalize("DeleteCampaign", Dict{String, Any}("campaignArn"=>campaignArn); aws_config=aws_config)
delete_campaign(campaignArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DeleteCampaign", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("campaignArn"=>campaignArn), args)); aws_config=aws_config)

"""
    DeleteDataset()

Deletes a dataset. You can't delete a dataset if an associated DatasetImportJob or SolutionVersion is in the CREATE PENDING or IN PROGRESS state. For more information on datasets, see CreateDataset.

# Required Parameters
- `datasetArn`: The Amazon Resource Name (ARN) of the dataset to delete.

"""
delete_dataset(datasetArn; aws_config::AWSConfig=global_aws_config()) = personalize("DeleteDataset", Dict{String, Any}("datasetArn"=>datasetArn); aws_config=aws_config)
delete_dataset(datasetArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DeleteDataset", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("datasetArn"=>datasetArn), args)); aws_config=aws_config)

"""
    DeleteDatasetGroup()

Deletes a dataset group. Before you delete a dataset group, you must delete the following:   All associated event trackers.   All associated solutions.   All datasets in the dataset group.

# Required Parameters
- `datasetGroupArn`: The ARN of the dataset group to delete.

"""
delete_dataset_group(datasetGroupArn; aws_config::AWSConfig=global_aws_config()) = personalize("DeleteDatasetGroup", Dict{String, Any}("datasetGroupArn"=>datasetGroupArn); aws_config=aws_config)
delete_dataset_group(datasetGroupArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DeleteDatasetGroup", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("datasetGroupArn"=>datasetGroupArn), args)); aws_config=aws_config)

"""
    DeleteEventTracker()

Deletes the event tracker. Does not delete the event-interactions dataset from the associated dataset group. For more information on event trackers, see CreateEventTracker.

# Required Parameters
- `eventTrackerArn`: The Amazon Resource Name (ARN) of the event tracker to delete.

"""
delete_event_tracker(eventTrackerArn; aws_config::AWSConfig=global_aws_config()) = personalize("DeleteEventTracker", Dict{String, Any}("eventTrackerArn"=>eventTrackerArn); aws_config=aws_config)
delete_event_tracker(eventTrackerArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DeleteEventTracker", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("eventTrackerArn"=>eventTrackerArn), args)); aws_config=aws_config)

"""
    DeleteFilter()

Deletes a filter.

# Required Parameters
- `filterArn`: The ARN of the filter to delete.

"""
delete_filter(filterArn; aws_config::AWSConfig=global_aws_config()) = personalize("DeleteFilter", Dict{String, Any}("filterArn"=>filterArn); aws_config=aws_config)
delete_filter(filterArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DeleteFilter", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("filterArn"=>filterArn), args)); aws_config=aws_config)

"""
    DeleteSchema()

Deletes a schema. Before deleting a schema, you must delete all datasets referencing the schema. For more information on schemas, see CreateSchema.

# Required Parameters
- `schemaArn`: The Amazon Resource Name (ARN) of the schema to delete.

"""
delete_schema(schemaArn; aws_config::AWSConfig=global_aws_config()) = personalize("DeleteSchema", Dict{String, Any}("schemaArn"=>schemaArn); aws_config=aws_config)
delete_schema(schemaArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DeleteSchema", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("schemaArn"=>schemaArn), args)); aws_config=aws_config)

"""
    DeleteSolution()

Deletes all versions of a solution and the Solution object itself. Before deleting a solution, you must delete all campaigns based on the solution. To determine what campaigns are using the solution, call ListCampaigns and supply the Amazon Resource Name (ARN) of the solution. You can't delete a solution if an associated SolutionVersion is in the CREATE PENDING or IN PROGRESS state. For more information on solutions, see CreateSolution.

# Required Parameters
- `solutionArn`: The ARN of the solution to delete.

"""
delete_solution(solutionArn; aws_config::AWSConfig=global_aws_config()) = personalize("DeleteSolution", Dict{String, Any}("solutionArn"=>solutionArn); aws_config=aws_config)
delete_solution(solutionArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DeleteSolution", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("solutionArn"=>solutionArn), args)); aws_config=aws_config)

"""
    DescribeAlgorithm()

Describes the given algorithm.

# Required Parameters
- `algorithmArn`: The Amazon Resource Name (ARN) of the algorithm to describe.

"""
describe_algorithm(algorithmArn; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeAlgorithm", Dict{String, Any}("algorithmArn"=>algorithmArn); aws_config=aws_config)
describe_algorithm(algorithmArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeAlgorithm", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("algorithmArn"=>algorithmArn), args)); aws_config=aws_config)

"""
    DescribeBatchInferenceJob()

Gets the properties of a batch inference job including name, Amazon Resource Name (ARN), status, input and output configurations, and the ARN of the solution version used to generate the recommendations.

# Required Parameters
- `batchInferenceJobArn`: The ARN of the batch inference job to describe.

"""
describe_batch_inference_job(batchInferenceJobArn; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeBatchInferenceJob", Dict{String, Any}("batchInferenceJobArn"=>batchInferenceJobArn); aws_config=aws_config)
describe_batch_inference_job(batchInferenceJobArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeBatchInferenceJob", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("batchInferenceJobArn"=>batchInferenceJobArn), args)); aws_config=aws_config)

"""
    DescribeCampaign()

Describes the given campaign, including its status. A campaign can be in one of the following states:   CREATE PENDING > CREATE IN_PROGRESS > ACTIVE -or- CREATE FAILED   DELETE PENDING > DELETE IN_PROGRESS   When the status is CREATE FAILED, the response includes the failureReason key, which describes why. For more information on campaigns, see CreateCampaign.

# Required Parameters
- `campaignArn`: The Amazon Resource Name (ARN) of the campaign.

"""
describe_campaign(campaignArn; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeCampaign", Dict{String, Any}("campaignArn"=>campaignArn); aws_config=aws_config)
describe_campaign(campaignArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeCampaign", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("campaignArn"=>campaignArn), args)); aws_config=aws_config)

"""
    DescribeDataset()

Describes the given dataset. For more information on datasets, see CreateDataset.

# Required Parameters
- `datasetArn`: The Amazon Resource Name (ARN) of the dataset to describe.

"""
describe_dataset(datasetArn; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeDataset", Dict{String, Any}("datasetArn"=>datasetArn); aws_config=aws_config)
describe_dataset(datasetArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeDataset", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("datasetArn"=>datasetArn), args)); aws_config=aws_config)

"""
    DescribeDatasetGroup()

Describes the given dataset group. For more information on dataset groups, see CreateDatasetGroup.

# Required Parameters
- `datasetGroupArn`: The Amazon Resource Name (ARN) of the dataset group to describe.

"""
describe_dataset_group(datasetGroupArn; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeDatasetGroup", Dict{String, Any}("datasetGroupArn"=>datasetGroupArn); aws_config=aws_config)
describe_dataset_group(datasetGroupArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeDatasetGroup", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("datasetGroupArn"=>datasetGroupArn), args)); aws_config=aws_config)

"""
    DescribeDatasetImportJob()

Describes the dataset import job created by CreateDatasetImportJob, including the import job status.

# Required Parameters
- `datasetImportJobArn`: The Amazon Resource Name (ARN) of the dataset import job to describe.

"""
describe_dataset_import_job(datasetImportJobArn; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeDatasetImportJob", Dict{String, Any}("datasetImportJobArn"=>datasetImportJobArn); aws_config=aws_config)
describe_dataset_import_job(datasetImportJobArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeDatasetImportJob", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("datasetImportJobArn"=>datasetImportJobArn), args)); aws_config=aws_config)

"""
    DescribeEventTracker()

Describes an event tracker. The response includes the trackingId and status of the event tracker. For more information on event trackers, see CreateEventTracker.

# Required Parameters
- `eventTrackerArn`: The Amazon Resource Name (ARN) of the event tracker to describe.

"""
describe_event_tracker(eventTrackerArn; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeEventTracker", Dict{String, Any}("eventTrackerArn"=>eventTrackerArn); aws_config=aws_config)
describe_event_tracker(eventTrackerArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeEventTracker", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("eventTrackerArn"=>eventTrackerArn), args)); aws_config=aws_config)

"""
    DescribeFeatureTransformation()

Describes the given feature transformation.

# Required Parameters
- `featureTransformationArn`: The Amazon Resource Name (ARN) of the feature transformation to describe.

"""
describe_feature_transformation(featureTransformationArn; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeFeatureTransformation", Dict{String, Any}("featureTransformationArn"=>featureTransformationArn); aws_config=aws_config)
describe_feature_transformation(featureTransformationArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeFeatureTransformation", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("featureTransformationArn"=>featureTransformationArn), args)); aws_config=aws_config)

"""
    DescribeFilter()

Describes a filter's properties.

# Required Parameters
- `filterArn`: The ARN of the filter to describe.

"""
describe_filter(filterArn; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeFilter", Dict{String, Any}("filterArn"=>filterArn); aws_config=aws_config)
describe_filter(filterArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeFilter", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("filterArn"=>filterArn), args)); aws_config=aws_config)

"""
    DescribeRecipe()

Describes a recipe. A recipe contains three items:   An algorithm that trains a model.   Hyperparameters that govern the training.   Feature transformation information for modifying the input data before training.   Amazon Personalize provides a set of predefined recipes. You specify a recipe when you create a solution with the CreateSolution API. CreateSolution trains a model by using the algorithm in the specified recipe and a training dataset. The solution, when deployed as a campaign, can provide recommendations using the GetRecommendations API.

# Required Parameters
- `recipeArn`: The Amazon Resource Name (ARN) of the recipe to describe.

"""
describe_recipe(recipeArn; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeRecipe", Dict{String, Any}("recipeArn"=>recipeArn); aws_config=aws_config)
describe_recipe(recipeArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeRecipe", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("recipeArn"=>recipeArn), args)); aws_config=aws_config)

"""
    DescribeSchema()

Describes a schema. For more information on schemas, see CreateSchema.

# Required Parameters
- `schemaArn`: The Amazon Resource Name (ARN) of the schema to retrieve.

"""
describe_schema(schemaArn; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeSchema", Dict{String, Any}("schemaArn"=>schemaArn); aws_config=aws_config)
describe_schema(schemaArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeSchema", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("schemaArn"=>schemaArn), args)); aws_config=aws_config)

"""
    DescribeSolution()

Describes a solution. For more information on solutions, see CreateSolution.

# Required Parameters
- `solutionArn`: The Amazon Resource Name (ARN) of the solution to describe.

"""
describe_solution(solutionArn; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeSolution", Dict{String, Any}("solutionArn"=>solutionArn); aws_config=aws_config)
describe_solution(solutionArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeSolution", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("solutionArn"=>solutionArn), args)); aws_config=aws_config)

"""
    DescribeSolutionVersion()

Describes a specific version of a solution. For more information on solutions, see CreateSolution.

# Required Parameters
- `solutionVersionArn`: The Amazon Resource Name (ARN) of the solution version.

"""
describe_solution_version(solutionVersionArn; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeSolutionVersion", Dict{String, Any}("solutionVersionArn"=>solutionVersionArn); aws_config=aws_config)
describe_solution_version(solutionVersionArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("DescribeSolutionVersion", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("solutionVersionArn"=>solutionVersionArn), args)); aws_config=aws_config)

"""
    GetSolutionMetrics()

Gets the metrics for the specified solution version.

# Required Parameters
- `solutionVersionArn`: The Amazon Resource Name (ARN) of the solution version for which to get metrics.

"""
get_solution_metrics(solutionVersionArn; aws_config::AWSConfig=global_aws_config()) = personalize("GetSolutionMetrics", Dict{String, Any}("solutionVersionArn"=>solutionVersionArn); aws_config=aws_config)
get_solution_metrics(solutionVersionArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("GetSolutionMetrics", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("solutionVersionArn"=>solutionVersionArn), args)); aws_config=aws_config)

"""
    ListBatchInferenceJobs()

Gets a list of the batch inference jobs that have been performed off of a solution version.

# Optional Parameters
- `maxResults`: The maximum number of batch inference job results to return in each page. The default value is 100.
- `nextToken`: The token to request the next page of results.
- `solutionVersionArn`: The Amazon Resource Name (ARN) of the solution version from which the batch inference jobs were created.
"""
list_batch_inference_jobs(; aws_config::AWSConfig=global_aws_config()) = personalize("ListBatchInferenceJobs"; aws_config=aws_config)
list_batch_inference_jobs(args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("ListBatchInferenceJobs", args; aws_config=aws_config)

"""
    ListCampaigns()

Returns a list of campaigns that use the given solution. When a solution is not specified, all the campaigns associated with the account are listed. The response provides the properties for each campaign, including the Amazon Resource Name (ARN). For more information on campaigns, see CreateCampaign.

# Optional Parameters
- `maxResults`: The maximum number of campaigns to return.
- `nextToken`: A token returned from the previous call to ListCampaigns for getting the next set of campaigns (if they exist).
- `solutionArn`: The Amazon Resource Name (ARN) of the solution to list the campaigns for. When a solution is not specified, all the campaigns associated with the account are listed.
"""
list_campaigns(; aws_config::AWSConfig=global_aws_config()) = personalize("ListCampaigns"; aws_config=aws_config)
list_campaigns(args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("ListCampaigns", args; aws_config=aws_config)

"""
    ListDatasetGroups()

Returns a list of dataset groups. The response provides the properties for each dataset group, including the Amazon Resource Name (ARN). For more information on dataset groups, see CreateDatasetGroup.

# Optional Parameters
- `maxResults`: The maximum number of dataset groups to return.
- `nextToken`: A token returned from the previous call to ListDatasetGroups for getting the next set of dataset groups (if they exist).
"""
list_dataset_groups(; aws_config::AWSConfig=global_aws_config()) = personalize("ListDatasetGroups"; aws_config=aws_config)
list_dataset_groups(args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("ListDatasetGroups", args; aws_config=aws_config)

"""
    ListDatasetImportJobs()

Returns a list of dataset import jobs that use the given dataset. When a dataset is not specified, all the dataset import jobs associated with the account are listed. The response provides the properties for each dataset import job, including the Amazon Resource Name (ARN). For more information on dataset import jobs, see CreateDatasetImportJob. For more information on datasets, see CreateDataset.

# Optional Parameters
- `datasetArn`: The Amazon Resource Name (ARN) of the dataset to list the dataset import jobs for.
- `maxResults`: The maximum number of dataset import jobs to return.
- `nextToken`: A token returned from the previous call to ListDatasetImportJobs for getting the next set of dataset import jobs (if they exist).
"""
list_dataset_import_jobs(; aws_config::AWSConfig=global_aws_config()) = personalize("ListDatasetImportJobs"; aws_config=aws_config)
list_dataset_import_jobs(args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("ListDatasetImportJobs", args; aws_config=aws_config)

"""
    ListDatasets()

Returns the list of datasets contained in the given dataset group. The response provides the properties for each dataset, including the Amazon Resource Name (ARN). For more information on datasets, see CreateDataset.

# Optional Parameters
- `datasetGroupArn`: The Amazon Resource Name (ARN) of the dataset group that contains the datasets to list.
- `maxResults`: The maximum number of datasets to return.
- `nextToken`: A token returned from the previous call to ListDatasetImportJobs for getting the next set of dataset import jobs (if they exist).
"""
list_datasets(; aws_config::AWSConfig=global_aws_config()) = personalize("ListDatasets"; aws_config=aws_config)
list_datasets(args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("ListDatasets", args; aws_config=aws_config)

"""
    ListEventTrackers()

Returns the list of event trackers associated with the account. The response provides the properties for each event tracker, including the Amazon Resource Name (ARN) and tracking ID. For more information on event trackers, see CreateEventTracker.

# Optional Parameters
- `datasetGroupArn`: The ARN of a dataset group used to filter the response.
- `maxResults`: The maximum number of event trackers to return.
- `nextToken`: A token returned from the previous call to ListEventTrackers for getting the next set of event trackers (if they exist).
"""
list_event_trackers(; aws_config::AWSConfig=global_aws_config()) = personalize("ListEventTrackers"; aws_config=aws_config)
list_event_trackers(args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("ListEventTrackers", args; aws_config=aws_config)

"""
    ListFilters()

Lists all filters that belong to a given dataset group.

# Optional Parameters
- `datasetGroupArn`: The ARN of the dataset group that contains the filters.
- `maxResults`: The maximum number of filters to return.
- `nextToken`: A token returned from the previous call to ListFilters for getting the next set of filters (if they exist).
"""
list_filters(; aws_config::AWSConfig=global_aws_config()) = personalize("ListFilters"; aws_config=aws_config)
list_filters(args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("ListFilters", args; aws_config=aws_config)

"""
    ListRecipes()

Returns a list of available recipes. The response provides the properties for each recipe, including the recipe's Amazon Resource Name (ARN).

# Optional Parameters
- `maxResults`: The maximum number of recipes to return.
- `nextToken`: A token returned from the previous call to ListRecipes for getting the next set of recipes (if they exist).
- `recipeProvider`: The default is SERVICE.
"""
list_recipes(; aws_config::AWSConfig=global_aws_config()) = personalize("ListRecipes"; aws_config=aws_config)
list_recipes(args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("ListRecipes", args; aws_config=aws_config)

"""
    ListSchemas()

Returns the list of schemas associated with the account. The response provides the properties for each schema, including the Amazon Resource Name (ARN). For more information on schemas, see CreateSchema.

# Optional Parameters
- `maxResults`: The maximum number of schemas to return.
- `nextToken`: A token returned from the previous call to ListSchemas for getting the next set of schemas (if they exist).
"""
list_schemas(; aws_config::AWSConfig=global_aws_config()) = personalize("ListSchemas"; aws_config=aws_config)
list_schemas(args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("ListSchemas", args; aws_config=aws_config)

"""
    ListSolutionVersions()

Returns a list of solution versions for the given solution. When a solution is not specified, all the solution versions associated with the account are listed. The response provides the properties for each solution version, including the Amazon Resource Name (ARN). For more information on solutions, see CreateSolution.

# Optional Parameters
- `maxResults`: The maximum number of solution versions to return.
- `nextToken`: A token returned from the previous call to ListSolutionVersions for getting the next set of solution versions (if they exist).
- `solutionArn`: The Amazon Resource Name (ARN) of the solution.
"""
list_solution_versions(; aws_config::AWSConfig=global_aws_config()) = personalize("ListSolutionVersions"; aws_config=aws_config)
list_solution_versions(args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("ListSolutionVersions", args; aws_config=aws_config)

"""
    ListSolutions()

Returns a list of solutions that use the given dataset group. When a dataset group is not specified, all the solutions associated with the account are listed. The response provides the properties for each solution, including the Amazon Resource Name (ARN). For more information on solutions, see CreateSolution.

# Optional Parameters
- `datasetGroupArn`: The Amazon Resource Name (ARN) of the dataset group.
- `maxResults`: The maximum number of solutions to return.
- `nextToken`: A token returned from the previous call to ListSolutions for getting the next set of solutions (if they exist).
"""
list_solutions(; aws_config::AWSConfig=global_aws_config()) = personalize("ListSolutions"; aws_config=aws_config)
list_solutions(args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("ListSolutions", args; aws_config=aws_config)

"""
    UpdateCampaign()

Updates a campaign by either deploying a new solution or changing the value of the campaign's minProvisionedTPS parameter. To update a campaign, the campaign status must be ACTIVE or CREATE FAILED. Check the campaign status using the DescribeCampaign API.  You must wait until the status of the updated campaign is ACTIVE before asking the campaign for recommendations.  For more information on campaigns, see CreateCampaign.

# Required Parameters
- `campaignArn`: The Amazon Resource Name (ARN) of the campaign.

# Optional Parameters
- `campaignConfig`: The configuration details of a campaign.
- `minProvisionedTPS`: Specifies the requested minimum provisioned transactions (recommendations) per second that Amazon Personalize will support.
- `solutionVersionArn`: The ARN of a new solution version to deploy.
"""
update_campaign(campaignArn; aws_config::AWSConfig=global_aws_config()) = personalize("UpdateCampaign", Dict{String, Any}("campaignArn"=>campaignArn); aws_config=aws_config)
update_campaign(campaignArn, args::AbstractDict{String, <:Any}; aws_config::AWSConfig=global_aws_config()) = personalize("UpdateCampaign", Dict{String, Any}(mergewith(_merge, Dict{String, Any}("campaignArn"=>campaignArn), args)); aws_config=aws_config)


text:
### A Pluto.jl notebook ###
# v0.17.3

using Markdown
using InteractiveUtils

# ╔═╡ 2b4aa264-4145-11ec-0d24-ab437ba0f84e
begin
    import Pkg
    # activate the shared project environment
    Pkg.activate(Base.current_project())

    using HMatrices, Plots, PlutoUI, LinearAlgebra, StaticArrays, WavePropBase, BenchmarkTools, LoopVectorization
	using WavePropBase.IO: PlotPoints, PlotTree
end

# ╔═╡ 9efa3b28-900c-4515-8aba-617bce2ddc68
PlutoUI.TableOfContents(;depth=2)

# ╔═╡ d1917856-64d1-4dab-9e05-40b1152217d2
begin
	const Point3D = SVector{3,Float64}
	m = 20_000
	X = Y = [Point3D(sin(θ)cos(ϕ),sin(θ)*sin(ϕ),cos(θ)) for (θ,ϕ) in zip(π*rand(m),2π*rand(m))]
end

# ╔═╡ bdfebc7a-a7a6-4fa6-8884-b4f86dc55cb3
begin
	Xclt = Yclt = ClusterTree(X)
	plot(PlotTree(),Xclt,mc=:red)
end

# ╔═╡ 306d493b-a558-4e1d-bad6-5c295da79e82
begin
	function G(x,y)
	    d = norm(x-y) + 1e-8
	    inv(4π*d)
	end
	K = KernelMatrix(G,X,Y)
end

# ╔═╡ 99041c6a-c12f-4a09-8bfd-cf769fd258a6
H = assemble_hmat(K,Xclt,Yclt;threads=false)

# ╔═╡ 02d89923-e478-4778-8b80-eaa3357a6a8c
b = @benchmark assemble_hmat($K,Xclt,Yclt;threads=false)

# ╔═╡ fca099bf-25c0-4ee8-b3f0-c042c4e5fde3
begin
	Xp = Yp = Trees.root_elements(Xclt)
	Kp = KernelMatrix(G,Xp,Yp)
	Hp = assemble_hmat(Kp,Xclt,Yclt;threads=false,global_index=false)
end

# ╔═╡ 37ed939e-1738-4ade-85d7-526c3c4ab536
bp = @benchmark assemble_hmat($Kp,Xclt,Yclt;threads=false,global_index=false)

# ╔═╡ c6729646-0b6c-4ea2-b692-62e1e8f6b97c
begin
	struct LaplaceMatrixVec <: AbstractKernelMatrix{Float64}
		X::Matrix{Float64}
		Y::Matrix{Float64}
	end
	Base.size(K::LaplaceMatrixVec) = size(K.X,1), size(K.Y,1)

	# convenience constructor based on Vector of StaticVector
	function LaplaceMatrixVec(_X::Vector{Point3D},_Y::Vector{Point3D})
	    X = reshape(reinterpret(Float64,_X), 3,:) |> transpose |> collect
	    Y = reshape(reinterpret(Float64,_Y), 3,:) |> transpose |> collect
	    LaplaceMatrixVec(X,Y)
	end

	function Base.getindex(K::LaplaceMatrixVec,i::Int,j::Int)
	    d2 = (K.X[i,1] - K.Y[j,1])^2 + (K.X[i,2] - K.Y[j,2])^2 + (K.X[i,3] - K.Y[j,3])^2
	    d = sqrt(d2) + 1e-8
	    return inv(4π*d)
	end
	function Base.getindex(K::LaplaceMatrixVec,I::UnitRange,J::UnitRange)
	    T = eltype(K)
	    m = length(I)
	    n = length(J)
	    Xv = view(K.X,I,:)
	    Yv = view(K.Y,J,:)
	    out = Matrix{T}(undef,m,n)
	    @turbo for j in 1:n
	        for i in 1:m
	            d2 = (Xv[i,1] - Yv[j,1])^2
	            d2 += (Xv[i,2] - Yv[j,2])^2
	            d2 += (Xv[i,3] - Yv[j,3])^2
	            d = sqrt(d2) + 1e-8
	            out[i,j] = inv(4*π*d)
	        end
	    end
	    return out
	end
	function Base.getindex(K::LaplaceMatrixVec,I::UnitRange,j::Int)
	    T = eltype(K)
	    m = length(I)
	    Xv = view(K.X,I,:)
	    out = Vector{T}(undef,m)
	    @turbo for i in 1:m
	            d2 = (Xv[i,1] -  K.Y[j,1])^2
	            d2 += (Xv[i,2] - K.Y[j,2])^2
	            d2 += (Xv[i,3] - K.Y[j,3])^2
	            d = sqrt(d2) + 1e-8
	            out[i] = inv(4*π*d)
		end
	    return out
	end
	function Base.getindex(adjK::Adjoint{<:Any,<:LaplaceMatrixVec},I::UnitRange,j::Int)
		K = parent(adjK)
	    T = eltype(K)
	    m = length(I)
		Yv = view(K.Y,I,:)
	    out = Vector{T}(undef,m)
	    @turbo for i in 1:m
	            d2 = (Yv[i,1] - K.X[j,1])^2
	            d2 += (Yv[i,2] - K.X[j,2])^2
	            d2 += (Yv[i,3] - K.X[j,3])^2
	            d = sqrt(d2) + 1e-8
	            out[i] = inv(4*π*d)
		end
	    return out
	end
end

# ╔═╡ 85de3038-88f8-442e-a2cd-5462f9552d12
md"""
# Custom kernels and vectorization

In this notebook we will cover how to implement your own `AbstractKernelMatrix` and explore ways to make the `HMatrix` assemble stage faster by exploiting vectorization.  We will focus on computing an approximation to the matrix $K$ with entries given in terms of a Greens function $G$:

```math
K_{i,j} = G(\boldsymbol{x}_i,\boldsymbol{y}_j), \quad \mbox{for} \quad \boldsymbol{x}_i \in X, \quad \boldsymbol{y}_j \in Y,
```
where $X=\{\boldsymbol{x}_k\}_{k=1}^m$ and $Y=\{\boldsymbol{y}_k\}_{k=1}^n$ are sets of points in $\mathbb{R}^3$ and $G(\boldsymbol{x},\boldsymbol{y}) = \frac{1}{4\pi ||\boldsymbol{x} - \boldsymbol{y} ||}$ is the Laplace's freespace Greens function.

After defining the point clouds $X$ and $Y$ (and the required tree data structures to cluster them) we will implement both a simple as well as a more complex (vectorized) implementation of this kernel matrix.

!!! note "Singular kernels"
	To avoid dealing with singularity at $\boldsymbol{x} = \boldsymbol{y}$, we will 		 simply add a small quantity `EPS = 1e-8`  to the distance. In an actual boundary integral equation solver, some special treatment is usually required to handle entries with $\boldsymbol{x} \approx \boldsymbol{y}$.
"""

# ╔═╡ 22b7faf3-b7e5-4c17-89eb-9f5118517188
md"""
## Point clouds and `ClusterTree`s

The first step is to create the point clouds $X$ and $Y$. We will take $X=Y$, and chose them to be points on a sphere of radius 1:
"""

# ╔═╡ dde4586f-d341-4bf8-a736-50ac311d7c73
md"""
Next we create a `ClusterTree` to organize the points in `X` into a hierarchy of axis-aligned bounding boxes:
"""

# ╔═╡ b29dc077-67ad-4e18-a428-5e832c9f188a
md"""
We can now proceed to the creation of a simple version of the matrix $K$.
"""

# ╔═╡ 78684c89-e7c5-46a0-949f-70e7166e14ef
md"""
## Simple implementation

The first implementation is made as simple as possible. We will make use of the `KernelMatrix` parametric type for this, so we only really need to provide an implementation of $G$:
"""

# ╔═╡ b488948d-dfcd-47ce-8b51-e04048e4eca7
md"""
With `K` at hand, we can now compress it using
"""

# ╔═╡ a513685e-17d3-4252-996b-a7e313561702
md"""
We benchmark the performance for comparison later:
"""

# ╔═╡ 4bfa0d77-58eb-4302-b5d1-05f35c5a500f
md"""
Next we will try to improve on the timing above by implementing a more efficient kernel matrix `K`.
"""

# ╔═╡ 69e228be-cea3-4379-9f9f-42b53abbe810
md"""
## Permuted kernel

Because the local indexing used internally to compute the `HMatrix` differs from the external (global) indexing used, the `getindex` method will not be called on contiguous entries of `K`. Providing a permuted kernel `Kp` may therefore improve performance since call to `Kp` will be made on `UnitRange` indices. This can be accomplished simply by defining `Kp` interms of the (permuted) elements in the `ClusterTree`, and passing the keyword argument `global_index=false` to the constructor to idicate that the kernel passed uses the local indexing system. The code below does precisely this:
"""

# ╔═╡ cd49f749-f0c9-4381-9d2b-03bdafc57584
md"""
Note that the structure of `Hp` is identical to that of `H`. Benchmarking this strategy yields some improvements already:
"""

# ╔═╡ 594751f2-b247-4e10-a43d-16242cc7bc97
md"""
## Vectorized implementation

We will now try to exploit the fact that, during the assemble of the `HMatrix` object, the kernel matrix `K` is called frequently to compute the dense and sparse blocks of the hierarchical matrix.

Dense blocks will be filled by performing the call `Kp[I′,J′]`, where `I′` and `J′` correspond to permutations of unit ranges `I::UnitRange` and `J::UnitRange`. The permutations are present because the indexing system used locally by the `HMatrix` is the one induced by its `rowtree` and `coltree`, which itselfs reorder the elements in the point clouds `X` and `Y` so that they are contiguous within a bounding box (see the documentation of `ClusterTree` and `HMatrix` for more details). Similarly, the low-rank blocks of the `HMatrix` will be assembled by sampling columns of `K` and of its `adjoint` through the following methods: `K[I′,j]` and `adjoint(K)[I′,j::Int]`. We will therefore implement efficient (vectorized) version of these `getindex` methods.

First, let us create a vecotrized kernel matrix withe the required `getindex` methods:
"""

# ╔═╡ 93d306cc-5901-4b0d-91fc-8eddecedad11
md"""
The `LaplaceMatrix` struct above has a vectorized implementatio of `getindex` for `UnitRange` objects. To make sure the `LaplaceMatrix` object is called on `UnitRange` indices, we must therefore constructed using the permuted element `Xp` and `Yp` which use the local index of the `HMatrix`. This is accomplished as follows:
"""

# ╔═╡ 71208d87-a9ed-410e-bad1-f770fc84d4db
begin
	Kv = LaplaceMatrixVec(Xp,Yp)
	Hv = assemble_hmat(Kv,Xclt,Yclt;global_index=false,threads=false)
end

# ╔═╡ 6eaa63e3-755d-482a-a113-98312b6f6fb1
md"""
Benchmarking the new implementation yields:
"""

# ╔═╡ 59fda22e-0547-4845-9207-31c7b526d2f6
bv = @benchmark assemble_hmat($Kv,Xclt,Yclt;threads=false,global_index=false)

# ╔═╡ 0b967aaf-271c-4153-acd6-2e5acff8dc75
md"""
Comparing the times we see that the vectorized implementation is the fastest, and the version with the permuted kernel is faster than the unpermuted one. But these results will vary depending on the machine, number of threads used, and many other factors, so that drawing a general conclusion regarding the speedup of vectorization is difficult. We expect, however, that the vectorized version should be no slower than the non-vectorized one.
"""

# ╔═╡ afc737f1-3523-413f-a73a-96a909580047
minimum(b),minimum(bp),minimum(bv)

# ╔═╡ 44f08eda-b5d3-40d0-aa8b-3cae9e137854
md"""
Finally, not that as expected all of these yield the same matrix, as indicated by the test below:
"""

# ╔═╡ 5fd9b390-ea5f-425c-a624-0cd4a146a897
begin
	x = rand(m)
	y  = H*x
	yv = Hv*x
	yp = Hp*x
	er = max(norm(y-yv)/norm(y),norm(y-yp)/norm(y))
	md"""
	Maximum error: $er
	"""
end

# ╔═╡ Cell order:
# ╠═2b4aa264-4145-11ec-0d24-ab437ba0f84e
# ╠═9efa3b28-900c-4515-8aba-617bce2ddc68
# ╟─85de3038-88f8-442e-a2cd-5462f9552d12
# ╟─22b7faf3-b7e5-4c17-89eb-9f5118517188
# ╠═d1917856-64d1-4dab-9e05-40b1152217d2
# ╟─dde4586f-d341-4bf8-a736-50ac311d7c73
# ╠═bdfebc7a-a7a6-4fa6-8884-b4f86dc55cb3
# ╟─b29dc077-67ad-4e18-a428-5e832c9f188a
# ╟─78684c89-e7c5-46a0-949f-70e7166e14ef
# ╟─306d493b-a558-4e1d-bad6-5c295da79e82
# ╟─b488948d-dfcd-47ce-8b51-e04048e4eca7
# ╠═99041c6a-c12f-4a09-8bfd-cf769fd258a6
# ╟─a513685e-17d3-4252-996b-a7e313561702
# ╠═02d89923-e478-4778-8b80-eaa3357a6a8c
# ╟─4bfa0d77-58eb-4302-b5d1-05f35c5a500f
# ╟─69e228be-cea3-4379-9f9f-42b53abbe810
# ╠═fca099bf-25c0-4ee8-b3f0-c042c4e5fde3
# ╟─cd49f749-f0c9-4381-9d2b-03bdafc57584
# ╠═37ed939e-1738-4ade-85d7-526c3c4ab536
# ╟─594751f2-b247-4e10-a43d-16242cc7bc97
# ╠═c6729646-0b6c-4ea2-b692-62e1e8f6b97c
# ╟─93d306cc-5901-4b0d-91fc-8eddecedad11
# ╠═71208d87-a9ed-410e-bad1-f770fc84d4db
# ╟─6eaa63e3-755d-482a-a113-98312b6f6fb1
# ╠═59fda22e-0547-4845-9207-31c7b526d2f6
# ╟─0b967aaf-271c-4153-acd6-2e5acff8dc75
# ╠═afc737f1-3523-413f-a73a-96a909580047
# ╟─44f08eda-b5d3-40d0-aa8b-3cae9e137854
# ╠═5fd9b390-ea5f-425c-a624-0cd4a146a897

ext: jl

max_stars_repo_name: WaveProp/HMatrices

max_forks_repo_name: WaveProp/HMatrices


text:
@testset "no_path" begin
  # Never any path
  no_path_neighbours(n) = Int64[]

  for algorithm in [astar, depthfirst, breadthfirst, iterative_deepening]
    res = algorithm(no_path_neighbours, 1, 2, maxdepth = 1)
    @test res.status == :nopath
    @test res.path == [1]
  end
end


text:
using Test

td = pwd()
for dir in ["basic","hamiltonian_zoo"]
    print("Including Test Dir:",dir,"\n")
    for file in readdir(td*"/"*dir,join=true)
        print("\tIncluding Test File:",file,"\n")
        include(file)
    end
end


text:
import Base: ==, *, /, \, isless, show, invperm

################################################################################

function arrayEquals{T}( x :: Array{T}, y :: Array{T} )
  n = length(x)
  if length(y) != n
    return false
  end
  for i = 1:n
    if x[i] != y[i]
      return false
    end
  end
  return true
end

function arrayCompare{T}( x :: Array{T}, y :: Array{T} )
  n = length(x)
  if length(y) != n
    return false
  end
  for i = 1:n
    if x[i] < y[i]
      return -1
    elseif x[i] > y[i]
      return 1
    end
  end
  return 0
end

################################################################################

# arr[i] == j  means what was in position j went to position i
type Perm
  arr :: Array{Int8}
end

function one( Perm, n :: Int )
  return Perm(1:n)
end

function ==( x :: Perm, y :: Perm )
  return arrayEquals( x.arr, y.arr )
end

function equalUpTo( x :: Perm, y :: Perm, n :: Int )
  for i = 1:n
    if x.arr[i] != y.arr[i]
      return false
    end
  end
  return true
end

function constUpTo( x :: Perm, n :: Int)
  return arrayEquals( 1:n, x[1:n] )
end

function numDifferent(x :: Perm)
  ret = 0
  for i = 1:length(x.arr)
    if x.arr[i] != i
      ret += 1
    end
  end
  return ret
end

# Do x, then do y
function *( x :: Perm, y :: Perm)
  a = copy( x.arr )
  permute!( a, y.arr )
  return Perm( a )
end
# Reverse x, then do y
function \( x :: Perm, y :: Perm )
  a = invperm( x.arr )
  permute!( a, y.arr )
  return Perm( a )
end
function square( x :: Perm )
  return x * x
end

function invperm( p :: Perm )
  return Perm( invperm(p.arr) )
end

function isless{T}(arr1 :: Array{T}, arr2 :: Array{T})
  for i = 1:length(arr1)
    if arr1[i] < arr2[i]
      return true
    end
    if arr1[i] > arr2[i]
      return false
    end
  end
  return false
end

function isless(x :: Perm, y :: Perm)
  for i = 1:length(x.arr)
    if x.arr[i] < y.arr[i]
      return true
    elseif x.arr[i] > y.arr[i]
      return false
    end
  end
  return false
end

################################################################################

function permFromCycle(n :: Int, arr :: Array{Int})
  p = one(Perm)
  q = copy(arr)
  mergeSort!(q)
  for i = 1:length(arr)
    p.arr[q[i]] = arr[i]
  end
  return p
end

################################################################################

function isodd( p :: Perm )
  n = length(p.arr)
  seen = Array{Bool}(n)
  ret = false
  for i = 1:n
    if seen[i]
      continue
    end
    if p.arr[i] == i
      seen[i] = true
      continue
    end
    #Start a cycle
    cycLengthOdd = true
    cycat = i
    while !seen[cycat]
      seen[cycat] = true
      cycat = p.arr[cycat]
      cycLengthOdd = !cycLengthOdd
    end
    if cycLengthOdd
      ret = !ret
    end
  end
  return ret
end
function iseven( p :: Perm )
  return !isodd(p)
end

################################################################################

function mergePartial!{T}(arr :: Array{T}, a :: Int, m :: Int, b :: Int)
  aa = Array{T}(b-a+1)
  i = a
  j = m+1
  k = 1
  while i <= m && j <= b
    if isless(arr[i], arr[j])
      aa[k] = arr[i]
      i += 1
    else
      aa[k] = arr[j]
      j += 1
    end
    k += 1
  end
  while i <= m
    aa[k] = arr[i]
    i += 1
    k += 1
  end
  while j <= b
    aa[k] = arr[j]
    j += 1
    k += 1
  end
  for x = a:b
    arr[x] = aa[x-a+1]
  end
  return arr
end

function mergeSortPartial!{T}(arr :: Array{T}, a :: Int, b :: Int)
  if a == b
    return arr
  end
  if b-a == 1
    if ! isless(arr[a], arr[b])
      c = arr[a]
      arr[a] = arr[b]
      arr[b] = c
    end
    return arr
  end
  m = div(b-a+1,2) + a - 1
  mergeSortPartial!(arr, a, m)
  mergeSortPartial!(arr, m+1, b)
  mergePartial!(arr, a, m, b)
end

function mergeSort!{T}(arr :: Array{T})
  if length(arr) == 1
    return arr
  end
  return mergeSortPartial!(arr, Int(1), length(arr))
end

################################################################################
# CenterPerm assumes the centers are matched up already,
# and describes how they will be shuffled around by a permutation,
# while keeping in mind the pieces' interchangeability.
# Each 4-tuple in the array (there are 6 of them) should be sorted ascendingly
# since we don't care about the ordering.

numCenterPiecesPerFace = 4
numCenterPieces = 24
type CenterPerm
  arr :: Array{Int8}
end

function mkNullCenterPerm()
  return CenterPerm(collect(1:numCenterPieces))
end

function ==( a :: CenterPerm, b :: CenterPerm )
  return arrayEquals( a, b )
end

function *( a :: CenterPerm, b :: CenterPerm )
  c = CenterPerm(copy(a))
  permute!(c,b)
  for i = 1:4:24
    mergeSortPartial!(c,i,i+3)
  end
end

################################################################################

type Position
  du :: Int8
  fb :: Int8
  lr :: Int8
  kind :: Int8 # 1 for side, 2 for center, 3 for corner
  code :: Int8 # its number in the permutation arrays
end

function allPositions()
  ret = Array{Position}(4,4,4)
  sidecode = 1
  centercode = 1
  cornercode = 1
  for du = 1:4
    duinner = (du==2 || du==3)
    for fb = 1:4
      fbinner = (fb==2 || fb==3)
      for lr = 1:4
        lrinner = (lr==2 || lr==3)
        if (lrinner && duinner && fbinner)
          continue
        end
        if (lrinner || duinner || fbinner)
          if ((lrinner && duinner) || (lrinner && fbinner) || (duinner && fbinner))
            # center
            ret[du,fb,lr] = Position(du,fb,lr,2,centercode)
            centercode += 1
          else
            # side
            ret[du,fb,lr] = Position(du,fb,lr,1,sidecode)
            sidecode += 1
          end
        else
          # corner
          ret[du,fb,lr] = Position(du,fb,lr,3,cornercode)
          cornercode += 1
        end
      end
    end
  end
  return ret
end

################################################################################

type Rotation
  axis :: Int8 # 1 for Bottom, 2 for Front, 3 for Right.
  layers :: Array{Int8} # 1 for outer layer, 2 for first inner layer, etc.
  direction :: Int8 # 1 for CW, 2 for 180, 3 for CCW.
  sidePerm :: Perm
  centerPerm :: Perm
  cornerPerm :: Perm
end

function ==(x :: Rotation, y :: Rotation)
  return x.axis == y.axis &&
    arrayEquals(x.layers,y.layers) &&
    x.direction == y.direction
end

function invrot( r :: Rotation )
  return Rotation(
    r.axis,
    r.layers,
    4 - r.direction,
    invperm(r.sidePerm),
    invperm(r.centerPerm),
    invperm(r.cornerPerm))
end

# rotate one point around an axis.
function rotatePointCW( du :: Int8, fb :: Int8, lr :: Int8, axis :: Int8 )
  if axis==1
    return ( du, lr, Int8(5)-fb )
  elseif axis==2
    return ( Int8(5)-lr, fb, du )
  else # if axis==3
    return ( fb, Int8(5)-du, lr )
  end
end
function rotatePoint( du :: Int8, fb :: Int8, lr :: Int8, axis :: Int8, dir :: Int8 )
  (a,b,c) = (du,fb,lr)
  for i = 1:dir
    (a,b,c) = rotatePointCW(a,b,c,axis)
  end
  return (a,b,c)
end

function mkRotation( allPos :: Array{Position}, axis :: Int8, layer :: Int8, dir :: Int8 )
  numPieces = (layer==1 || layer==4) ? 16 : 12
  sidePermArr = collect(1:24)
  centerPermArr = collect(1:24)
  cornerPermArr = collect(1:8)

  poss = Array{Position}(numPieces)
  k = 1
  for i = 1:4
    for j = 1:4
      if (i==1 || i==4 || j==1 || j==4 || layer==1 || layer==4)
        if axis==1
          poss[k] = allPos[layer,i,j]
        elseif axis==2
          poss[k] = allPos[i,layer,j]
        else
          poss[k] = allPos[i,j,layer]
        end
        k += 1
      end
    end
  end

  for i = 1:numPieces
    (newdu,newfb,newlr) = rotatePoint( poss[i].du, poss[i].fb, poss[i].lr, axis, dir )
    newpos = allPos[newdu,newfb,newlr]
    if newpos.kind==1
      sidePermArr[newpos.code] = poss[i].code
    elseif newpos.kind==2
      centerPermArr[newpos.code] = poss[i].code
    else
      cornerPermArr[newpos.code] = poss[i].code
    end
  end

  return Rotation(axis, [layer], dir, Perm(sidePermArr), Perm(centerPermArr), Perm(cornerPermArr))
end

function combineRotations( a :: Rotation, b :: Rotation )
  if a.axis != b.axis
    return -1
  elseif a.direction != b.direction
    return -2
  elseif length(a.layers) != 1 && length(b.layers) != 1
    return -3
  end
  layers = Array{Int8}(0)
  append!(layers, a.layers)
  append!(layers, b.layers)
  return Rotation(a.axis, layers, a.direction, a.sidePerm*b.sidePerm, a.centerPerm*b.centerPerm, a.cornerPerm*b.cornerPerm)
end

function rotationSequenceMakesSense( a :: Rotation, b :: Rotation )
  return a.axis != b.axis
  if a.axis == b.axis
    if a.direction==b.direction
      if length(a.layers)==1 && length(b.layers)==1
        if a.layers[1]*b.layers[1] == 2 # 1&2
          return false
        elseif a.layers[1]*b.layers[1] == 12 # 3&4
          return false
        end
      end
    end
    if arrayEquals(a.layers, b.layers)
      return false
    elseif arrayEquals(a.layers, [Int8(1),Int8(2)]) && arrayEquals(b.layers, [Int8(3),Int8(4)])
      return false
    elseif arrayEquals(b.layers, [Int8(1),Int8(2)]) && arrayEquals(a.layers, [Int8(3),Int8(4)])
      return false
    elseif a.direction == b.direction
      # doing 1 CW then 2 CW is stupid.
      if arrayEquals(a.layers, [Int8(1)]) && arrayEquals(a.layers, [Int8(2)])
        return false
      elseif arrayEquals(a.layers, [Int8(2)]) && arrayEquals(a.layers, [Int8(1)])
        return false
      elseif arrayEquals(a.layers, [Int8(4)]) && arrayEquals(a.layers, [Int8(3)])
        return false
      elseif arrayEquals(a.layers, [Int8(3)]) && arrayEquals(a.layers, [Int8(4)])
        return false
      elseif length(a.layers) + length(b.layers) >= 3 && ((a.layers[1] <= 2 && b.layers[1] >= 3) || (b.layers[1] <= 2 && a.layers[1] >= 3))
        return false
      end
    end
    if a.direction == (4 - b.direction)
      # doing 1 CW then 12 CCW is stupid for example.
      if length(a.layers)==1
        for i = 1:length(b.layers)
          if b.layers[i] == a.layers[1]
            return false
          end
        end
      end
      if length(b.layers)==1
        for i = 1:length(a.layers)
          if a.layers[i] == b.layers[1]
            return false
          end
        end
      end
    end
  end
  return true
end

function rotationTripleMakesSense( a :: Rotation, b :: Rotation, c :: Rotation )
  return a.axis != b.axis && b.axis != c.axis
  if a.axis == b.axis && a.axis == c.axis
    return false
  end
  return true
end

function allRotations()

  allPos = allPositions()

  ret = Array{Rotation}(3*7*3)
  i = 1
  for ax = 1:3
    for dir = 1:3
      for lay = 2:4
        ret[i] = mkRotation( allPos, Int8(ax), Int8(lay), Int8(dir) )
        i += 1
      end
      ret[i] = combineRotations( ret[i-3], ret[i-2] )
      i += 1
      ret[i] = combineRotations( ret[i-4], ret[i-2] )
      i += 1
      ret[i] = combineRotations( ret[i-4], ret[i-3] )
      i += 1
      ret[i] = combineRotations( ret[i-6], ret[i-1] )
      i += 1
    end
  end

  return ret

end

################################################################################

type Algorithm
  rotations :: Array{Rotation}
  sidePerm :: Perm
  centerPerm :: Perm
  cornerPerm :: Perm
  #sortingCode :: BigInt
end

function mkAlgorithm(rots :: Array{Rotation})
  s = rots[1].sidePerm
  ce = rots[1].centerPerm
  co = rots[1].cornerPerm
  for i = 2:length(rots)
    s = s * rots[i].sidePerm
    ce = ce * rots[i].centerPerm
    co = co * rots[i].cornerPerm
  end
  return Algorithm(rots, s, ce, co)
end

function sortCodifyAlgorithm!( a :: Algorithm )
  factor = BigInt(1)
  index = 32
  ret = BigInt(0)
  for i in 24:-1:17
    ret += factor * (a.sidePerm.arr[i]-1)
    #index += 1
    factor *= index
  end
  for i in 8:-1:1
    ret += factor * (a.cornerPerm.arr[i]-1)
    #index += 1
    factor *= index
  end
  for i in 16:-1:1
    ret += factor * (a.sidePerm.arr[i]-1)
    #index += 1
    factor *= index
  end
  #a.sortingCode = ret
end
function sortCodifyAlgorithms!( algs :: Array{Algorithm} )
  for i = 1:length(algs)
    sortCodifyAlgorithm!(algs[i])
  end
end

function oneRotationAlgorithm( rot :: Rotation )
  return mkAlgorithm( [rot] )
end

function isless(a1 :: Algorithm, a2 :: Algorithm)
  #return isless(a1.sidePerm, a2.sidePerm)

  #return a1.sortingCode < a2.sortingCode

  # now sort by first 16 side piece destinations, then by corners, then by last 8 side pieces
  c = arrayCompare(a1.sidePerm.arr[1:16], a2.sidePerm.arr[1:16])
  if c < 0
    return true
  elseif c > 0
    return false
  end
  c = arrayCompare(a1.cornerPerm.arr, a2.cornerPerm.arr)
  if c < 0
    return true
  elseif c > 0
    return false
  end
  c = arrayCompare(a1.sidePerm.arr[17:24], a2.sidePerm.arr[17:24])
  return c < 0
end

function invalg( a :: Algorithm )
  n = length(a.rotations)
  rots = Array{Rotation}( n )
  for i = 1:n
    rots[n+1-i] = invrot( a.rotations[i] )
  end
  return Algorithm(
    rots,
    invperm(a.sidePerm),
    invperm(a.centerPerm),
    invperm(a.cornerPerm) )
end

function *(a1 :: Algorithm, a2 :: Algorithm)
  rots = Array{Rotation}( length(a1.rotations) + length(a2.rotations) )
  for i = 1:length(a1.rotations)
    rots[i] = a1.rotations[i]
  end
  for i = 1:length(a2.rotations)
    rots[i + length(a1.rotations)] = a2.rotations[i]
  end
  return Algorithm(
    rots,
    a1.sidePerm * a2.sidePerm,
    a1.centerPerm * a2.centerPerm,
    a1.cornerPerm * a2.cornerPerm )
end

function isNotSandwichedAlgorithm( a :: Algorithm )
  if length(a.rotations)>2
    ri = a.rotations[1]
    rf = a.rotations[length(a.rotations)]
    if ri.axis==1 && rf.axis==1 && ri.direction == 4-rf.direction
      rilayers = Array{Int8}(0)
      if ri.layers[length(ri.layers)] == Int8(4)
        append!(rilayers,ri.layers[1:(length(ri.layers)-1)])
      else
        append!(rilayers,ri.layers)
      end
      rflayers = Array{Int8}(0)
      if rf.layers[length(rf.layers)] == Int8(4)
        append!(rflayers,rf.layers[1:(length(rf.layers)-1)])
      else
        append!(rflayers,rf.layers)
      end
      if arrayEquals(rilayers, rflayers)
        return false
      end
    end
  end
  return true
end

################################################################################

function show(r :: Rotation)
  layers = r.layers
  axis = r.axis
  dir = r.direction
  if (r.layers[1]>2)
    layers = Array{Int8}(length(r.layers))
    for i = 1:length(r.layers)
      layers[i] = 5 - r.layers[length(layers)+1-i]
    end
    axis = -axis
    dir = 4-dir
  end

  for i = (length(layers)+1):4
    print(" ")
  end
  for i = 1:length(layers)
    print(layers[i])
  end

  if axis==1
    print(" D ")
  elseif axis==2
    print(" F ")
  elseif axis==3
    print(" L ")
  elseif axis==-1
    print(" U ")
  elseif axis==-2
    print(" B ")
  else # if axis==-3
    print(" R ")
  end
  if dir==1
    println("CW")
  elseif dir==2
    println("CW Twice")
  else
    println("CCW")
  end
end
function show(alg :: Algorithm)
  for i = 1:length(alg.rotations)
    show(alg.rotations[i])
  end
  println()
end
function show(algs :: Array{Algorithm})
  println()
  for i = 1:length(algs)
    println("  Algorithm ", i)
    println("---------------")
    show(algs[i])
    println()
  end
end

################################################################################

function allAlgs(moves :: Array{Rotation}, numMoves)
  if numMoves==1
    retalgs = Array{Algorithm}(length(moves))
    for i = 1:length(moves)
      retalgs[i] = oneRotationAlgorithm(moves[i])
    end
    return retalgs
  end
  subsequents = allAlgs(moves, numMoves-1)
  ret = Array{Algorithm}( 0 )
  for m = 1:length(moves)
    oneMoveAlg = oneRotationAlgorithm( moves[m] )
    for s = 1:length(subsequents)
      if rotationSequenceMakesSense( oneMoveAlg.rotations[1], subsequents[s].rotations[1] )
        if length(subsequents[s].rotations)<2 || rotationTripleMakesSense( oneMoveAlg.rotations[1], subsequents[s].rotations[1], subsequents[s].rotations[2] )
          append!(ret, [oneMoveAlg * subsequents[s]])
        end
      end
    end
  end
  i = 1
  for m = (length(moves)*length(subsequents) + 1):length(ret)
    ret[m] = subsequents[i]
    i += 1
  end
  return ret
end

topRot = mkRotation( allPositions(), Int8(1), Int8(4), Int8(1) )

function appendOneWaySelfCross(fun, isok, firstisok, src, i, j, dest)
  temparr = Array{typeof(fun(src[i],src[j]))}( (j-i+1)*(j-i+1) )
  #println("temparr from i=",i," to j=",j)
  m = 1
  for k = i:j
    a = src[k]
    if firstisok(a)
      for l = (k+1):j
        #if l==k
        #  continue
        #end
        c = fun( a, src[l] )
        if isok(c)
          temparr[m] = c
          m += 1
        end
      end
    end
  end
  #filter!( isok, temparr[1:(m-1)] )
  append!( dest, temparr[1:(m-1)] )
end
function funForAppendOneWaySelfCross(a :: Algorithm, b :: Algorithm)
  return invalg(a) * b
end

function algsCounteractEachOther( a :: Algorithm, b :: Algorithm, numConstSides, numConstCorners)
  # return true iff a.sortingCode and b.sortingCode have the same first 20 digits
  #if abs(a.sortingCode - b.sortingCode) >= BigInt(32)^12
  #  return false
  #end
  return equalUpTo(a.sidePerm, b.sidePerm, numConstSides) && equalUpTo(a.cornerPerm, b.cornerPerm, numConstCorners)
end

function isrelevantalg( a :: Algorithm )
  n = length(a.rotations)
  lastRot = a.rotations[n]
  # if last rotation is the top
  if lastRot.axis == 1 && arrayEquals(lastRot.layers, [Int8(4)])
    return false
  elseif !isNotSandwichedAlgorithm(a)
    return false
  elseif isstupidalg(topRot, a)
    return false
  elseif isredundantalg(a)
    return false
  # filter out algs that mess up the centers
  elseif !keepsCenters(a)
    return false
  # filter out algs that mess up the corners
  elseif !keepsBottomCorners(a)
    return false
  end

  return true
end

function isrelevantstartofalg( halfAlg :: Algorithm )

  n = length(halfAlg.rotations)
  firstrot = halfAlg.rotations[n]

  # filter out algs that start with R or L
  # (because you can do the same by starting with F)
  if firstrot.axis == 3
    return false
  # filter out algs that start with the back layer
  # (because you can do the same by starting with F)
  elseif firstrot.axis == 2 && firstrot.layers[1] >= 3
    return false
  # filter out algs that start with CCW
  # (because you can mirror them)
  elseif firstrot.direction == 1
    return false
  # filter out algs that start with rotating the top
  elseif firstrot.axis == 1 && arrayEquals(firstrot.layers, [Int8(4)])
    return false
  end

  return true
end

function bucketFunc( a :: Algorithm, level )
  if level > 16
    return a.cornerPerm.arr[level-16]
  else
    return a.sidePerm.arr[level]
  end
end

function bucketSortPartial!(arr :: Array{Algorithm}, i, j, numBuckets, maxLevel, level, mergeSortAfter)
  temparr = Array{Algorithm}(numBuckets, length(arr))
  temparrSizes = Array{Int32}(numBuckets)
  for k = 1:numBuckets
    temparrSizes[k] = 0
  end
  for k = i:j
    b = bucketFunc(arr[k], level)
    temparrSizes[b] += 1
    temparr[ b, temparrSizes[b] ] = arr[k]
  end
  k = i
  for b = 1:numBuckets
    for l = 1:temparrSizes[b]
      arr[k] = temparr[b,l]
      k += 1
    end
  end
  if level<maxLevel
    k = i
    for b = 1:numBuckets
      bucketSortPartial!(arr, k, k + temparrSizes[b]-1, numBuckets, maxLevel, level+1, mergeSortAfter)
      k += temparrSizes[b]
    end
  elseif mergeSortAfter
    k = i
    for b = 1:numBuckets
      if temparrSizes[b]>1
        mergeSortPartial!(arr, k, k + temparrSizes[b]-1)
      end
      k += temparrSizes[b]
    end
  end
end

function bucketSort!( arr :: Array{Algorithm}, numBuckets, maxLevel, mergeSortAfter)
  bucketSortPartial!( arr, 1, length(arr), numBuckets, maxLevel, 1, mergeSortAfter)
end

# algorithms for a given layer.
function layeredAlgs(moves :: Array{Rotation}, numMovesHalved, layer :: Int)
  # numConstSides is number is side pieces (from bottom up) that should remain in place
  numConstSides = layer==2 ? 8 : layer==3 ? 12 : 16
  numConstCorners = 4
  permSize = length( moves[1].sidePerm.arr )
  halfAlgs = allAlgs(moves, numMovesHalved)
  println("done halfAlgs, the number of them is ", length(halfAlgs), ".")

  #sortCodifyAlgorithms!(halfAlgs)
  #println("And now they're sort-codified.")

  numConst = numConstSides+numConstCorners
  numBuckets = 24
  bucketSort!( halfAlgs, numBuckets, 1, true )
  println("And now the halfAlgs are sorted.")

  nHalf = length(halfAlgs)

  #println(sum(halfAlgsGroupedSizes))
  #println(nHalf)

  ret = Array{Algorithm}(0)
  n = nHalf
  i = 1
  while i < n
    if algsCounteractEachOther(halfAlgs[i], halfAlgs[i+1], numConstSides, numConstCorners)
      j = i+1
      while j < n && algsCounteractEachOther(halfAlgs[i], halfAlgs[j+1], numConstSides, numConstCorners)
        j += 1
      end
      #println("Found a group! bucket=",bucket," i=",i," j=",j)
      appendOneWaySelfCross(funForAppendOneWaySelfCross, isrelevantalg, isrelevantstartofalg, halfAlgs, i, j, ret)
      i = j+1
    else
      i += 1
    end
  end

  return ret
end

################################################################################

function isstupidalg(rot :: Rotation, a :: Algorithm)
  ap = a.sidePerm
  p = rot.sidePerm
  for i = 1:4
    if ap==p
      return true
    end
    p = p * rot.sidePerm
  end
  return false
end

function algContainsNoStupidity( a :: Algorithm )
  for i = 1:(length(a.rotations)-1)
    if !rotationSequenceMakesSense(a.rotations[i], a.rotations[i+1])
      return false
    elseif i <= (length(a.rotations)-2)
      if !rotationTripleMakesSense(a.rotations[i], a.rotations[i+1], a.rotations[i+2])
        return false
      end
    end
  end
  return true
end

function theyRotateSameLayer( r1 :: Rotation, r2 :: Rotation )
  if r1.axis != r2.axis
    return false
  elseif !arrayEquals(r1.layers,r2.layers)
    return false
  end
  return true
end

function isredundantalg( alg :: Algorithm )
  for i = 1:(length(alg.rotations)-1)
    if theyRotateSameLayer(alg.rotations[i], alg.rotations[i+1])
      return true
    end
  end
  return false
end

function issideodd( r :: Rotation )
  if r.direction==2
    return false
  end
  ret = false
  for i = 1:length(r.layers)
    if r.layers[i]==2 || r.layers[i]==3
      ret = !ret
    end
  end
  return ret
end
function iscornerodd( r :: Rotation )
  if r.direction==2
    return false
  end
  ret = false
  for i = 1:length(r.layers)
    if r.layers[i]==1 || r.layers[i]==4
      ret = !ret
    end
  end
  return ret
end
function issideodd( a :: Algorithm )
  ret = false
  for i = 1:length(a.rotations)
    if issideodd(a.rotations[i])
      ret = !ret
    end
  end
  return ret
end
function iscornerodd( a :: Algorithm )
  ret = false
  for i = 1:length(a.rotations)
    if iscornerodd(a.rotations[i])
      ret = !ret
    end
  end
  return ret
end

################################################################################

function keepsCenters( alg :: Algorithm )
  csi = [1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6,6,6,6]
  csf = copy(csi)
  permute!(csf, alg.centerPerm.arr)
  return arrayEquals(csi,csf)
end

function keepsBottomCorners( alg :: Algorithm )
  csi = Array{Int8}(8)
  csf = Array{Int8}(8)
  for i = 1:4
    csi[i] = Int8(i)
    csf[i] = Int8(i)
    csi[i + 4] = Int8(5)
    csf[i + 4] = Int8(5)
  end
  permute!(csf, alg.cornerPerm.arr)
  return arrayEquals(csi,csf)
end

################################################################################

function getAlgs4(numMoves)

  allPos = allPositions()
  allRot = allRotations()
  topRot = mkRotation( allPos, Int8(1), Int8(4), Int8(1) )

  x = layeredAlgs(allRot, div(numMoves,2), 4)
  println("Done layeredAlgs! Their number is ", length(x), ".")

  filter!(algContainsNoStupidity, x)

  return x

end

function algs4( numMoves )
  a4 = getAlgs4( numMoves )
  show(a4)
end

################################################################################

# one move may have several different rotations on the same axis,
# so one layer can be CW the other CCW in one move for example.
type AxisMove
  axis :: Int8
  rotations :: Array{Tuple{Int8,Int8}}
  sidePerm :: Perm
  centerPerm :: Perm
  cornerPerm :: Perm
end

################################################################################

max_stars_repo_name: clauderichard/RubikSearch

max_issues_repo_name: clauderichard/RubikSearch

max_forks_repo_name: clauderichard/RubikSearch


text:
# Stochastic needs ΔW = s^(1/2).
#=
β = 0.6
σ = (t,y) -> β*y/(4.0s)
u0 = 1.5Newton
prob = SDEProblem(f,σ,u0)

sol =solve(prob::SDEProblem,[0,1],dt=(1/2^4)Second,save_everystep=true,alg=:EM)
sol =solve(prob::SDEProblem,[0,1],dt=(1/2^4)Second,save_everystep=true,alg=:SRIW1)

TEST_PLOT && plot(sol)

u0 = [1.5Newton 2.0Newton
      3.0Newton 1.0Newton]

prob = SDEProblem(f,σ,u0)

sol =solve(prob::SDEProblem,[0,1],dt=(1/2^4)Second,save_everystep=true,alg=:EM)
sol =solve(prob::SDEProblem,[0,1],dt=(1/2^4)Second,save_everystep=true,alg=:SRIW1)
=#


text:
facts("HO-SVD") do
r = 2
T = rand_kruskal3(r, (10, 20, 30), false)

context("no residuals calculation") do
    @time factors = hosvd(T, r)
    @fact length(factors.factors) --> ndims(T)
    @fact map(size, factors.factors) --> (collect(zip(size(T), (r, r, r)))...)
    @fact size(factors.core) --> (r, r, r)
    @fact rel_residue(factors) --> isnan
end

context("core reconstruction and residuals") do
    @time factors = hosvd(T, r, compute_error=true)
    @fact size(factors.core) --> (r, r, r)
    @fact rel_residue(factors) --> less_than(1e-5)
end

context("core dimension equal to the original dimension") do
    @time factors = hosvd(randn(10, 20, 30), (10, 20, 5), compute_error=true)
    @fact size(factors.core) --> (10, 20, 5)
    @fact rel_residue(factors) --> less_than(1e-5)
end

end


text:
function J234Gravity(Body::T, PosRsph::Vector{Float64}) where {T <: abstractCelestialBody}
  g = zeros(3)
  #Convert δ (latitude) into co-latitude α
  α = pi/2 - PosRsph[3];
  Re_r = mean_radius(Body)/PosRsph[1]
  J2planet = j2(Body)
  J3planet = j3(Body)
  J4planet = j4(Body)
  muplanet = mu(Body)
  R = PosRsph[1]
  sec_a = 0
  #Division by zero check
  cos(α) == 0 ? sec_a = 1e20 : sec_a = 1/cos(α)
  #Fourth order correction
  g[1] += -5/8*J4planet*Re_r^4*(35*cos(α)^4 - 30*cos(α)^2 + 3)
  g[3] += 5/6*J4planet*Re_r^2*(7*cos(α)^2 - 1)
  #Third order correction
  g[1] += -2*J3planet*Re_r^3*cos(α)*(5*cos(α)^2 - 1)
  g[3] += 0.5*J3planet*Re_r*sec_a*(5*cos(α)^2 - 1)
  #Second order correction
  g[1] += -1.5*J2planet*Re_r^2*(3*cos(α)^2 - 1)
  g[3] = 3*muplanet/R^2*Re_r^2*sin(α)*cos(α)*(g[3] + J2planet)
  #Central field term
  g[1] = -muplanet/R^2*(1+g[1])
  #transform to V-frame
  return g = [-g[3], g[2], g[1]]
end


text:
using Random
using CUDAdrv
using CUDAnative
using CuArrays
using StaticArrays
using GPUifyLoops # for @unroll macro

# Base.@propagate_inbounds function Base.getindex(v::MArray, i::Int)
#     @boundscheck checkbounds(v,i)
#     T = eltype(v)
#
#     @assert isbitstype(T) "Only isbits is supported on the device"
#     return GC.@preserve v begin
#         ptr  = pointer_from_objref(v)
#         dptr = reinterpret(CUDAnative.DevicePtr{T, CUDAnative.AS.Local}, ptr)
#         unsafe_load(dptr, i)
#     end
# end
#
# Base.@propagate_inbounds function Base.setindex!(v::MArray, val, i::Int)
#     @boundscheck checkbounds(v,i)
#     T = eltype(v)
#
#     @assert isbitstype(T) "Only isbits is supported on the device"
#     GC.@preserve v begin
#     ptr  = pointer_from_objref(v)
#         dptr = reinterpret(CUDAnative.DevicePtr{T, CUDAnative.AS.Local}, ptr)
#         unsafe_store!(dptr, convert(T, val), i)
#     end
#
#     return val
# end

for (f, T) in Base.Iterators.product((:add, :mul, :sub), (Float32, Float64))
    name = Symbol("$(f)_float_contract")
    if T === Float32
        llvmt = "float"
    elseif T === Float64
        llvmt = "double"
    end

    ir = """
    %x = f$f contract $llvmt %0, %1
    ret $llvmt %x
    """
    @eval begin
        # the @pure is necessary so that we can constant propagate.
        Base.@pure function $name(a::$T, b::$T)
            @Base._inline_meta
            Base.llvmcall($ir, $T, Tuple{$T, $T}, a, b)
        end
    end
end

import Base: +, *, -

+(a::Float64, b::Float64) = add_float_contract(a, b)
+(a::Float32, b::Float32) = add_float_contract(a, b)
*(a::Float64, b::Float64) = mul_float_contract(a, b)
*(a::Float32, b::Float32) = mul_float_contract(a, b)
-(a::Float64, b::Float64) = sub_float_contract(a, b)
-(a::Float32, b::Float32) = sub_float_contract(a, b)

# note the order of the fields below is also assumed in the code.
const _nstate = 5
const _ρ, _U, _V, _W, _E = 1:_nstate

const _nvgeo = 14
const _ξx, _ηx, _ζx, _ξy, _ηy, _ζy, _ξz, _ηz, _ζz, _MJ, _MJI,
       _x, _y, _z = 1:_nvgeo

Base.@irrational grav 9.81 BigFloat(9.81)
Base.@irrational gdm1 0.4 BigFloat(0.4)

function volumerhs!(::Val{N}, rhs, Q, vgeo, gravity, D, nelem) where {N}
    Nq = N + 1
    Np = Nq ^ 3

    s_D = @cuStaticSharedMem eltype(D) (Nq, Nq)
    s_F = @cuStaticSharedMem eltype(Q) (Nq, Nq, _nstate)
    s_G = @cuStaticSharedMem eltype(Q) (Nq, Nq, _nstate)

    r_rhsρ = MArray{Tuple{Nq},eltype(rhs)}(undef)
    r_rhsU = MArray{Tuple{Nq},eltype(rhs)}(undef)
    r_rhsV = MArray{Tuple{Nq},eltype(rhs)}(undef)
    r_rhsW = MArray{Tuple{Nq},eltype(rhs)}(undef)
    r_rhsE = MArray{Tuple{Nq},eltype(rhs)}(undef)

    e = blockIdx().x
    j = threadIdx().y
    i = threadIdx().x

    common_gid = i + (j-1)*Nq + (e-1)*Np*_nvgeo
    common_qid = i + (j-1)*Nq + (e-1)*Np*_nstate

    # fetch D into shared
    @inbounds s_D[i, j] = D[i, j]

    @inbounds @unroll for k in 1:Nq
        r_rhsρ[k] = zero(eltype(rhs))
        r_rhsU[k] = zero(eltype(rhs))
        r_rhsV[k] = zero(eltype(rhs))
        r_rhsW[k] = zero(eltype(rhs))
        r_rhsE[k] = zero(eltype(rhs))
    end

    @inbounds @unroll for k in 1:Nq
        sync_threads()

        gid = common_gid + (k-1)*Nq*Nq

        # Load values will need into registers
        MJ = ldg(vgeo, gid + (_MJ - 1) * Np)
        ξx = ldg(vgeo, gid + (_ξx - 1) * Np)
        ξy = ldg(vgeo, gid + (_ξy - 1) * Np)
        ξz = ldg(vgeo, gid + (_ξz - 1) * Np)
        ηx = ldg(vgeo, gid + (_ηx - 1) * Np)
        ηy = ldg(vgeo, gid + (_ηy - 1) * Np)
        ηz = ldg(vgeo, gid + (_ηz - 1) * Np)
        ζx = ldg(vgeo, gid + (_ζx - 1) * Np)
        ζy = ldg(vgeo, gid + (_ζy - 1) * Np)
        ζz = ldg(vgeo, gid + (_ζz - 1) * Np)
        z  = ldg(vgeo, gid + (_z - 1) * Np)

        qid = common_qid + (k-1)*Nq*Nq

        U  = ldg(Q, qid + (_U - 1) * Np)
        V  = ldg(Q, qid + (_V - 1) * Np)
        W  = ldg(Q, qid + (_W - 1) * Np)
        ρ  = ldg(Q, qid + (_ρ - 1) * Np)
        E  = ldg(Q, qid + (_E - 1) * Np)

        P = gdm1*(E - (U^2 + V^2 + W^2)/(2*ρ) - ρ*gravity*z)

        ρinv = 1 / ρ

        fluxρ_x = U
        fluxU_x = ρinv * U * U + P
        fluxV_x = ρinv * U * V
        fluxW_x = ρinv * U * W
        fluxE_x = ρinv * U * (E + P)

        fluxρ_y = V
        fluxU_y = ρinv * V * U
        fluxV_y = ρinv * V * V + P
        fluxW_y = ρinv * V * W
        fluxE_y = ρinv * V * (E + P)

        fluxρ_z = W
        fluxU_z = ρinv * W * U
        fluxV_z = ρinv * W * V
        fluxW_z = ρinv * W * W + P
        fluxE_z = ρinv * W * (E + P)

        s_F[i, j,  _ρ] = MJ * (ξx * fluxρ_x + ξy * fluxρ_y + ξz * fluxρ_z)
        s_F[i, j,  _U] = MJ * (ξx * fluxU_x + ξy * fluxU_y + ξz * fluxU_z)
        s_F[i, j,  _V] = MJ * (ξx * fluxV_x + ξy * fluxV_y + ξz * fluxV_z)
        s_F[i, j,  _W] = MJ * (ξx * fluxW_x + ξy * fluxW_y + ξz * fluxW_z)
        s_F[i, j,  _E] = MJ * (ξx * fluxE_x + ξy * fluxE_y + ξz * fluxE_z)

        s_G[i, j,  _ρ] = MJ * (ηx * fluxρ_x + ηy * fluxρ_y + ηz * fluxρ_z)
        s_G[i, j,  _U] = MJ * (ηx * fluxU_x + ηy * fluxU_y + ηz * fluxU_z)
        s_G[i, j,  _V] = MJ * (ηx * fluxV_x + ηy * fluxV_y + ηz * fluxV_z)
        s_G[i, j,  _W] = MJ * (ηx * fluxW_x + ηy * fluxW_y + ηz * fluxW_z)
        s_G[i, j,  _E] = MJ * (ηx * fluxE_x + ηy * fluxE_y + ηz * fluxE_z)

        r_Hρ = MJ * (ζx * fluxρ_x + ζy * fluxρ_y + ζz * fluxρ_z)
        r_HU = MJ * (ζx * fluxU_x + ζy * fluxU_y + ζz * fluxU_z)
        r_HV = MJ * (ζx * fluxV_x + ζy * fluxV_y + ζz * fluxV_z)
        r_HW = MJ * (ζx * fluxW_x + ζy * fluxW_y + ζz * fluxW_z)
        r_HE = MJ * (ζx * fluxE_x + ζy * fluxE_y + ζz * fluxE_z)

        # one shared access per 10 flops
        @unroll for n = 1:Nq
            Dnk = s_D[n, k]

            r_rhsρ[n] += Dnk * r_Hρ
            r_rhsU[n] += Dnk * r_HU
            r_rhsV[n] += Dnk * r_HV
            r_rhsW[n] += Dnk * r_HW
            r_rhsE[n] += Dnk * r_HE
        end

        r_rhsW[k] -= MJ * ρ * gravity

        sync_threads()

        # loop of ξ-grid lines
        @unroll for n = 1:Nq
            Dni = s_D[n, i]
            Dnj = s_D[n, j]

            r_rhsρ[k] += Dni * s_F[n, j, _ρ]
            r_rhsρ[k] += Dnj * s_G[i, n, _ρ]

            r_rhsU[k] += Dni * s_F[n, j, _U]
            r_rhsU[k] += Dnj * s_G[i, n, _U]

            r_rhsV[k] += Dni * s_F[n, j, _V]
            r_rhsV[k] += Dnj * s_G[i, n, _V]

            r_rhsW[k] += Dni * s_F[n, j, _W]
            r_rhsW[k] += Dnj * s_G[i, n, _W]

            r_rhsE[k] += Dni * s_F[n, j, _E]
            r_rhsE[k] += Dnj * s_G[i, n, _E]
        end
    end

    @inbounds @unroll for k in 1:Nq
        gid = common_gid + (k-1)*Nq*Nq
        MJI = ldg(vgeo, gid + (_MJI - 1) * Np)

        qid = common_qid + (k-1)*Nq*Nq

        rhs[qid + (_U - 1) * Np] += MJI*r_rhsU[k]
        rhs[qid + (_V - 1) * Np] += MJI*r_rhsV[k]
        rhs[qid + (_W - 1) * Np] += MJI*r_rhsW[k]
        rhs[qid + (_ρ - 1) * Np] += MJI*r_rhsρ[k]
        rhs[qid + (_E - 1) * Np] += MJI*r_rhsE[k]
    end

    nothing
end

function main()
    N = 4
    nelem = 4000
    ntrials = 1
    DFloat = Float32

    rnd = MersenneTwister(0)

    Nq = N + 1
    Q = 1 .+ CuArray(rand(rnd, DFloat, Nq, Nq, Nq, _nstate, nelem))
    Q[:, :, :, _E, :] .+= 20
    vgeo = CuArray(rand(rnd, DFloat, Nq, Nq, Nq, _nvgeo, nelem))

    # Make sure the entries of the mass matrix satisfy the inverse relation
    vgeo[:, :, :, _MJ, :] .+= 3
    vgeo[:, :, :, _MJI, :] .= 1 ./ vgeo[:, :, :, _MJ, :]

    D = CuArray(rand(rnd, DFloat, Nq, Nq))

    rhs = CuArray(zeros(DFloat, Nq, Nq, Nq, _nstate, nelem))

    @cuda(threads=(N+1, N+1), blocks=nelem, maxregs=255,
          volumerhs!(Val(N), rhs, Q, vgeo, DFloat(grav), D, nelem))

    CUDAdrv.@profile  begin
        @cuda(threads=(N+1, N+1), blocks=nelem, maxregs=255,
              volumerhs!(Val(N), rhs, Q, vgeo, DFloat(grav), D, nelem))
    end

    CUDAnative.@device_code dir="jlir" begin
        @cuda(threads=(N+1, N+1), blocks=nelem, maxregs=255,
              volumerhs!(Val(N), rhs, Q, vgeo, DFloat(grav), D, nelem))
    end

    nothing
end

main()


text:
using MatchCore
using Test

@testset "MatchCore.jl" begin
    # Write your own tests here.
end


text:
using OrdinaryDiffEq
using Trixi

###############################################################################
# semidiscretization of the Lattice-Boltzmann equations for the D2Q9 scheme

equations = LatticeBoltzmannEquations2D(Ma=0.05, Re=2000)

"""
    initial_condition_couette_unsteady(x, t, equations::LatticeBoltzmannEquations2D)

Initial state for an *unsteady* Couette flow setup, which is also the exact solution for the
incompressible Navier-Stokes equations. To be used in combination with
[`boundary_condition_couette`](@ref) and [`boundary_condition_noslip_wall`](@ref). In the limit,
this setup will converge to the state set in [`initial_condition_couette_steady`](@ref).
"""
function initial_condition_couette_unsteady(x, t, equations::LatticeBoltzmannEquations2D)
  @unpack L, u0, rho0, nu = equations

  x1, x2 = x
  v1 = u0*x2/L
  for m in 1:100
    lambda_m = m * pi / L
    v1 += 2 * u0 * (-1)^m/(lambda_m * L) * exp(-nu * lambda_m^2 * t) * sin(lambda_m * x2)
  end

  rho = 1
  v2 = 0

  return equilibrium_distribution(rho, v1, v2, equations)
end
initial_condition = initial_condition_couette_unsteady

"""
    boundary_condition_couette(u_inner, orientation, direction, x, t,
                               surface_flux_function,
                               equations::LatticeBoltzmannEquations2D)

Moving *upper* wall boundary condition for a Couette flow setup. To be used in combination with
[`boundary_condition_noslip_wall`](@ref) for the lower wall and
[`boundary_condition_periodic`](@ref) for the lateral boundaries.
"""
function boundary_condition_couette(u_inner, orientation, direction, x, t,
                                    surface_flux_function,
                                    equations::LatticeBoltzmannEquations2D)
  return boundary_condition_moving_wall_ypos(u_inner, orientation, direction, x, t,
                                             surface_flux_function, equations)
end

function boundary_condition_moving_wall_ypos(u_inner, orientation, direction, x, t,
                                             surface_flux_function,
                                             equations::LatticeBoltzmannEquations2D)
  @assert direction == 4 "moving wall assumed in +y direction"

  @unpack rho0, u0, weights, c_s = equations
  cs_squared = c_s^2

  pdf1 = u_inner[3] + 2 * weights[1] * rho0 * u0 / cs_squared
  pdf2 = u_inner[2] # outgoing
  pdf3 = u_inner[1] + 2 * weights[3] * rho0 * (-u0) / cs_squared
  pdf4 = u_inner[2]
  pdf5 = u_inner[5] # outgoing
  pdf6 = u_inner[6] # outgoing
  pdf7 = u_inner[5] + 2 * weights[7] * rho0 * (-u0) / cs_squared
  pdf8 = u_inner[6] + 2 * weights[8] * rho0 * u0 / cs_squared
  pdf9 = u_inner[9]

  u_boundary = SVector(pdf1, pdf2, pdf3, pdf4, pdf5, pdf6, pdf7, pdf8, pdf9)

  # Calculate boundary flux (u_inner is "left" of boundary, u_boundary is "right" of boundary)
  return surface_flux_function(u_inner, u_boundary, orientation, equations)
end
boundary_conditions = (
                       x_neg=boundary_condition_periodic,
                       x_pos=boundary_condition_periodic,
                       y_neg=boundary_condition_noslip_wall,
                       y_pos=boundary_condition_couette,
                      )

solver = DGSEM(polydeg=3, surface_flux=flux_godunov)

coordinates_min = (0.0, 0.0)
coordinates_max = (1.0, 1.0)
mesh = TreeMesh(coordinates_min, coordinates_max,
                initial_refinement_level=3,
                periodicity=(true, false),
                n_cells_max=10_000,)

semi = SemidiscretizationHyperbolic(mesh, equations, initial_condition, solver,
                                    boundary_conditions=boundary_conditions)

###############################################################################
# ODE solvers, callbacks etc.

tspan = (0.0, 40.0)
ode = semidiscretize(semi, tspan)

summary_callback = SummaryCallback()

analysis_interval = 1000
analysis_callback = AnalysisCallback(semi, interval=analysis_interval)

alive_callback = AliveCallback(analysis_interval=analysis_interval)

# Custom solution variables: normalize velocities by reference speed `u0`
@inline function macroscopic_normalized(u, equations::LatticeBoltzmannEquations2D)
  macroscopic = cons2macroscopic(u, equations)
  rho, v1, v2, p = macroscopic

  # Use `typeof(macroscopic)` to avoid having to explicitly add `using StaticArrays`
  convert(typeof(macroscopic), (rho, v1/equations.u0, v2/equations.u0, p))
end
Trixi.varnames(::typeof(macroscopic_normalized), equations::LatticeBoltzmannEquations2D) = ("rho", "v1_normalized", "v2_normalized", "p")

save_solution = SaveSolutionCallback(interval=1000,
                                     save_initial_solution=true,
                                     save_final_solution=true,
                                     solution_variables=macroscopic_normalized)

stepsize_callback = StepsizeCallback(cfl=1.0)

collision_callback = LBMCollisionCallback()

callbacks = CallbackSet(summary_callback,
                        analysis_callback, alive_callback,
                        save_solution,
                        stepsize_callback,
                        collision_callback)

###############################################################################
# run the simulation

sol = solve(ode, CarpenterKennedy2N54(williamson_condition=false),
            dt=1.0, # solve needs some value here but it will be overwritten by the stepsize_callback
            save_everystep=false, callback=callbacks);
summary_callback() # print the timer summary


text:
module MetabolicFluxAnalysis

# package code goes here
export LC , Flux , Species , Network , add, parseLC , parse_linconstr , isless , compute_emu_decomp, EMURxn , getN
export SAS, RAS , PAS , SpeciesEMU  ,  constructSpeciesEMU , SpeciesEMURxn , create_emu_graph
export get_positions_of_semu
export parseFluxi , create_ast_from_emu_graph , parseFlux
export ASTEMUSim ,  ASTEMUSimStep , ASTMatrix , ASTNumNode , ASTEMUMassRatio , ASTFluxRate , ASTSymmOp , ASTRealValue

export construct_mds , construct_all_mass_ratios_for_measurement , get_configured_reactands

export lc2polyhedron , get_flux_polyhedron
export remove_redundant_constraints , mve_run , mve_solve , solve_chebyshev

# basic workflows:
export compile_julia_fwsim , evaluate_flux2mds , evaluate_flux2sse , create_julia_ile_evaluator
export ast_to_opencl_01

# io
export export_json , parse_ftbl_data , ftbl_to_fluxi , import_cobra

# wx parametrization:
export WXTransform , is_wx_flux , get_wx_flux_pair ,  get_wxflux_net , get_wxflux_xch , get_optimal_wx_parametrization

include("polyhedra/polyhedra.jl")
include("base/lincomb.jl")
include("base/isotopomer.jl")
include("base/networks.jl")
include("fluxi/fluxiparser.jl")
include("fluxi/emudecomp.jl")
include("fluxi/emugraph.jl")
include("fluxi/fluxi_ast.jl")

abstract type FWSim
end

include("fluxi/fluxi_julia_backend.jl")
include("fluxi/fluxi_opencl_backend.jl")

include("polyhedra/mve_solver.jl")
include("polyhedra/samp_unif.jl")
include("polyhedra/polytope_volume.jl")

include("workflows/mfa_basics.jl")
include("workflows/parametrization_basics.jl")

include("polyhedra/flux_parametrizations_01.jl")

include("io/json_io.jl")
include("io/import_ftbl.jl")
include("io/import_cobra.jl")

end # module