OptaPlanner的新約束表達方式 Constraint Streams

  有好些時間沒有寫過關於OptaPlanner的東西了,其實近半年來,OptaPlanner還是推出了不少有用、好用的新特性。包括本文講到的以Stream接口實現評分編程。關於OptraPlanner的約束詳細用法,可以參考官方資料.

  最近幾個版本推出的新功能、特性中,有不少功能還處於初始探索階段,甚至有些功能還未成體系,包括我在上一篇文件中推出的SolverManger實現批量異步規劃。此功能尚未支持ProblemChanged接口,從而無法實現Realtime Planning. 因此,若需要將這些功能應用於項目實踐,還請自行作詳細調查分析,以免在項目中處於進退兩難境地。

PS. 任何技術都一樣,功能、版本越新,帶來的收益越高,當然需要面對的風險也越高。

  對OptaPlanner有初步認識都清楚,我們使用OptaPlanner規劃建模時,需要在模型中表達一系列約束,以描述各個業務實體的約束和規劃的優化目標。以往通常有兩種方式實現評分邏輯(詳細可分爲3種)。分別是:

1. 通過Drools腳本中的Rule來描述約束並進行評分;

2. 通過Java編寫評分邏輯,通過Java編輯評分邏輯又分爲:

   2.1. Java簡易評分 - Easy Java score calculation

   2.2. Java增量評分 - Incremental Java score calculation

  從7.31版本開始提供的constraint streams屬於Java增量評分的一種。在普通的Java增量評分中,我們需要針對各個約束邏輯,編輯相應的判斷,並在滿足一定條件後,通過ScoreHolder對象進行記分。引擎會將各個層次的分數進行累加,成爲當前方案的總分。Constraint Streams的原理也一樣,只是通過強大的Stream特性,令評分邏輯更爲簡潔,使用更短的代碼即可實現更豐富的邏輯描述。

  關於Java的Stream特性(Java1.8及以後的版本纔出現)的使用方法,可自行通過其它網絡資源學習,本文假設讀者熟悉Java Stream的各種用法。

我們先以一個簡單的示例說明Constraint streams接口的使用方法:

private int doNotAssignAnn() {
        int softScore = 0;
        schedule.getShiftList().stream()
                .filter(Shift::isEmployeeAnn)
                .forEach(shift -> {
                    softScore -= 1;
                });
        return softScore;
    }

  通過上述代碼塊是一Java簡易評分的示例,從方法名doNotAssignAnn就很容易理解到,該約束的作用是“使得任務不要分配給Ann”。我們知道在OptaPlanner裏,評分通常都是負數,表示懲罰一個行爲,令引擎找出儘可能規避這種行爲的方案。示例中使用了Java的Stream功能進行判斷和過濾。其邏輯是:從班次列表中找出所有分配給了Ann的班次,對每一個滿足這個條件的班次進行扣分,並把分數加總作爲方法的返回值。

  那麼同樣的約束要求,使用Constraint Stream應該如何實現呢?見以下代碼:

private Constraint doNotAssignAnn(ConstraintFactory factory) {
        return factory.from(Shift.class)
                .filter(Shift::isEmployeeAnn)
                .penalize("Don't assign Ann", HardSoftScore.ONE_SOFT);
    }

  先要提醒一下,與Java簡單評分法類似(評分類需要實現EasyScoreCalculator接口),OptaPlanner的Constraint Stream提供一個名爲ConstraintProvider的接口,實現評分的類需要實現這個接口,這個接口只有一個需要實現的方法 - defineConstraints,它傳入ConstrantFactory類,返回一個Constraint數組,數組的元素就是已進行了評分和懲罰的各個約束對象。上面的代碼中可以看到,doNotAssignAnn方法返回一個Constraint對象,這個對象表示了對Ann被分配到的班次數的懲罰分數。上述代碼可以看到,我們只需要對ConstraintFactory的對象factory進行Stream操作,一步即可完成判斷、過濾和懲罰三個操作,完成這些操作後會得到一個操作過的Contraint對象,返回該對象即可。上述代碼中,對於factory的三步操作也相當明瞭,大家可以自己理解。

  但是對於一些更復雜的判斷,其實現步驟與模式也一樣,只不過需要編寫一些更復雜的Lambda表達式來進行判斷、過濾和各種運算。如下代碼:

  private Constraint requiredCpuPowerTotal(ConstraintFactory factory) {
        return factory.from(CloudProcess.class)
                .groupBy(CloudProcess::getComputer, sum(CloudProcess::getRequiredCpuPower))
                .filter((computer, requiredCpuPower) -> requiredCpuPower > computer.getCpuPower())
                .penalize("requiredCpuPowerTotal",
                        HardSoftScore.ONE_HARD,
                        (computer, requiredCpuPower) -> requiredCpuPower - computer.getCpuPower());
    }

  該代碼是CloudBalance中用於,計算限制一臺計劃機被分配超出其CPU運算能力的約束。大家可以回想,或從官方示例中看一下CloudBalance的其中一個最基本約束 - 每臺計算機所分得的CPU需求,不可超過該計算機的可用CPU能力。

  因此,可以看到,factory除了過from操作獲得所有Process對象,通過filter對Process進行過濾,通過penalize進行計分外。factory對象還有一個groupBy方法,用於對所有Process中的Computer進行分組並加總每一組(即每個Computer)的所有CPU計算能力需求量。因此,在filter方法中,就找出那些超出CPU能力的Computer(即分組),在penalize方法中,對整所有超出CPU需求中的計算進行扣分,扣分值是超出部分。

  由此可能,OptaPlanner提供的Constraint Stream可以進行更復雜的條件判斷,至於這種方法是否更好用,就取決於大家對Stream(類似C#中的Linq)的熟悉程度。

  至於整個Constraint Stream代碼的結果方式,即上面提到的實現ConstraintProvider接口的代碼如下(摘自官方示例CloudBalance):

public class CloudBalancingConstraintProvider implements ConstraintProvider {

    @Override
    public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
        return new Constraint[] {
                requiredCpuPowerTotal(constraintFactory),
                requiredMemoryTotal(constraintFactory),
                requiredNetworkBandwidthTotal(constraintFactory),
                computerCost(constraintFactory)
        };
    }

    // ************************************************************************
    // Hard constraints
    // ************************************************************************

    private Constraint requiredCpuPowerTotal(ConstraintFactory constraintFactory) {
        return constraintFactory.from(CloudProcess.class)
                .groupBy(CloudProcess::getComputer, sum(CloudProcess::getRequiredCpuPower))
                .filter((computer, requiredCpuPower) -> requiredCpuPower > computer.getCpuPower())
                .penalize("requiredCpuPowerTotal",
                        HardSoftScore.ONE_HARD,
                        (computer, requiredCpuPower) -> requiredCpuPower - computer.getCpuPower());
    }
.
.
.

  重複提示一下,Constraint Stream功能是7.31版纔開始提供的功能,從功能接口上應該是未夠成功的,如果需要在項目中實現一些更爲複雜的約束描述,建議暫時還是不要直接使用。在OptaPlanner的用戶手冊中,也有相關的提示;大家看情況而用。

  最近一段時間OptaPlanner更新算比較頻繁,但從網站上的更新內容看到,很多版本儘管隔了很長時間,但接口上的更新內容卻不多。我向Geoffrey查詢過,他表示這些版本更多的情況是在實現一些引擎內部的優化和一些新的內部運算功能,但這些功能不一定反映到API上,因此對於我們使用者來說,並沒有太大的變化。可是如果大家也跟進將OptaPlanner的程序包也更新到最新版本,就會發現,很多一些常用的接口、方法,都已經被標準爲將爲放棄,從Javadocs上可以看到一些當前版本被標識爲@Deprecated的方法、成員,已明確說明將在8.x中停止使用。

  上述功能希望可以幫大家理解並應用OptaPlanner的第四種評分方式。

有好些時間沒有寫過關於OptaPlanner的東西了,其實近半年來,OptaPlanner還是推出了不少有用、好用的新特性。包括本文講到的以Stream接口實現評分編程。關於OptraPlanner的約束詳細用法,可以參考官方資料:

Constraint streams score calculationdocs.optaplanner.org

最近幾個版本推出的新功能、特性中,有不少功能還處於初始探索階段,甚至有些功能還未成體系,包括我在上一篇文件中推出的SolverManger實現批量異步規劃。此功能尚未支持ProblemChanged接口,從而無法實現Realtime Planning. 因此,若需要將這些功能應用於項目實踐,還請自行作詳細調查分析,以免在項目中處於進退兩難境地。

PS. 任何技術都一樣,功能、版本越新,帶來的收益越高,當然需要面對的風險也越高。

對OptaPlanner有初步認識都清楚,我們使用OptaPlanner規劃建模時,需要在模型中表達一系列約束,以描述各個業務實體的約束和規劃的優化目標。以往通常有兩種方式實現評分邏輯(詳細可分爲3種)。分別是:

  1. 通過Drools腳本中的Rule來描述約束並進行評分;
  2. 通過Java編寫評分邏輯,通過Java編輯評分邏輯又分爲:
    1. Java簡易評分 - Easy Java score calculation
    2. Java增量評分 - Incremental Java score calculation

從7.31版本開始提供的constraint streams屬於Java增量評分的一種。在普通的Java增量評分中,我們需要針對各個約束邏輯,編輯相應的判斷,並在滿足一定條件後,通過ScoreHolder對象進行記分。引擎會將各個層次的分數進行累加,成爲當前方案的總分。Constraint Streams的原理也一樣,只是通過強大的Stream特性,令評分邏輯更爲簡潔,使用更短的代碼即可實現更豐富的邏輯描述。

關於Java的Stream特性(Java1.8及以後的版本纔出現)的使用方法,可自行通過其它網絡資源學習,本文假設讀者熟悉Java Stream的各種用法。

我們先以一個簡單的示例說明Constraint streams接口的使用方法:

private int doNotAssignAnn() {
        int softScore = 0;
        schedule.getShiftList().stream()
                .filter(Shift::isEmployeeAnn)
                .forEach(shift -> {
                    softScore -= 1;
                });
        return softScore;
    }

通過上述代碼塊是一Java簡易評分的示例,從方法名doNotAssignAnn就很容易理解到,該約束的作用是“使得任務不要分配給Ann”。我們知道在OptaPlanner裏,評分通常都是負數,表示懲罰一個行爲,令引擎找出儘可能規避這種行爲的方案。示例中使用了Java的Stream功能進行判斷和過濾。其邏輯是:從班次列表中找出所有分配給了Ann的班次,對每一個滿足這個條件的班次進行扣分,並把分數加總作爲方法的返回值。

那麼同樣的約束要求,使用Constraint Stream應該如何實現呢?見以下代碼:

private Constraint doNotAssignAnn(ConstraintFactory factory) {
        return factory.from(Shift.class)
                .filter(Shift::isEmployeeAnn)
                .penalize("Don't assign Ann", HardSoftScore.ONE_SOFT);
    }

先要提醒一下,與Java簡單評分法類似(評分類需要實現EasyScoreCalculator接口),OptaPlanner的Constraint Stream提供一個名爲ConstraintProvider的接口,實現評分的類需要實現這個接口,這個接口只有一個需要實現的方法 - defineConstraints,它傳入ConstrantFactory類,返回一個Constraint數組,數組的元素就是已進行了評分和懲罰的各個約束對象。上面的代碼中可以看到,doNotAssignAnn方法返回一個Constraint對象,這個對象表示了對Ann被分配到的班次數的懲罰分數。上述代碼可以看到,我們只需要對ConstraintFactory的對象factory進行Stream操作,一步即可完成判斷、過濾和懲罰三個操作,完成這些操作後會得到一個操作過的Contraint對象,返回該對象即可。上述代碼中,對於factory的三步操作也相當明瞭,大家可以自己理解。

但是對於一些更復雜的判斷,其實現步驟與模式也一樣,只不過需要編寫一些更復雜的Lambda表達式來進行判斷、過濾和各種運算。如下代碼:

  private Constraint requiredCpuPowerTotal(ConstraintFactory factory) {
        return factory.from(CloudProcess.class)
                .groupBy(CloudProcess::getComputer, sum(CloudProcess::getRequiredCpuPower))
                .filter((computer, requiredCpuPower) -> requiredCpuPower > computer.getCpuPower())
                .penalize("requiredCpuPowerTotal",
                        HardSoftScore.ONE_HARD,
                        (computer, requiredCpuPower) -> requiredCpuPower - computer.getCpuPower());
    }

該代碼是CloudBalance中用於,計算限制一臺計劃機被分配超出其CPU運算能力的約束。大家可以回想,或從官方示例中看一下CloudBalance的其中一個最基本約束 - 每臺計算機所分得的CPU需求,不可超過該計算機的可用CPU能力。

因此,可以看到,factory除了過from操作獲得所有Process對象,通過filter對Process進行過濾,通過penalize進行計分外。factory對象還有一個groupBy方法,用於對所有Process中的Computer進行分組並加總每一組(即每個Computer)的所有CPU計算能力需求量。因此,在filter方法中,就找出那些超出CPU能力的Computer(即分組),在penalize方法中,對整所有超出CPU需求中的計算進行扣分,扣分值是超出部分。

由此可能,OptaPlanner提供的Constraint Stream可以進行更復雜的條件判斷,至於這種方法是否更好用,就取決於大家對Stream(類似C#中的Linq)的熟悉程度。

至於整個Constraint Stream代碼的結果方式,即上面提到的實現ConstraintProvider接口的代碼如下(摘自官方示例CloudBalance):

public class CloudBalancingConstraintProvider implements ConstraintProvider {

    @Override
    public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
        return new Constraint[] {
                requiredCpuPowerTotal(constraintFactory),
                requiredMemoryTotal(constraintFactory),
                requiredNetworkBandwidthTotal(constraintFactory),
                computerCost(constraintFactory)
        };
    }

    // ************************************************************************
    // Hard constraints
    // ************************************************************************

    private Constraint requiredCpuPowerTotal(ConstraintFactory constraintFactory) {
        return constraintFactory.from(CloudProcess.class)
                .groupBy(CloudProcess::getComputer, sum(CloudProcess::getRequiredCpuPower))
                .filter((computer, requiredCpuPower) -> requiredCpuPower > computer.getCpuPower())
                .penalize("requiredCpuPowerTotal",
                        HardSoftScore.ONE_HARD,
                        (computer, requiredCpuPower) -> requiredCpuPower - computer.getCpuPower());
    }
.
.
.

重複提示一下,Constraint Stream功能是7.31版纔開始提供的功能,從功能接口上應該是未夠成功的,如果需要在項目中實現一些更爲複雜的約束描述,建議暫時還是不要直接使用。在OptaPlanner的用戶手冊中,也有相關的提示;大家看情況而用。

最近一段時間OptaPlanner更新算比較頻繁,但從網站上的更新內容看到,很多版本儘管隔了很長時間,但接口上的更新內容卻不多。我向Geoffrey查詢過,他表示這些版本更多的情況是在實現一些引擎內部的優化和一些新的內部運算功能,但這些功能不一定反映到API上,因此對於我們使用者來說,並沒有太大的變化。可是如果大家也跟進將OptaPlanner的程序包也更新到最新版本,就會發現,很多一些常用的接口、方法,都已經被標準爲將爲放棄,從Javadocs上可以看到一些當前版本被標識爲@Deprecated的方法、成員,已明確說明將在8.x中停止使用。

 

本系列文章在公衆號不定時連載,請關注公衆號(讓APS成爲可能)及時接收,二維碼:

 


如需瞭解更多關於Optaplanner的應用,請發電郵致:[email protected]
或到討論組發表你的意見:
若有需要可添加本人微信(13631823503)或QQ(12977379)實時溝通,但因本人日常工作繁忙,通過微信,QQ等工具可能無法深入溝通,較複雜的問題,建議以郵件或討論組方式提出。(討論組屬於google郵件列表,國內網絡可能較難訪問,需自行解決)

 

上述功能希望可以幫大家理解並應用OptaPlanner的第四種評分方式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章