2020年3月31日火曜日

経験年数7年以上に変更

経験年数6年から、7年に変更依頼がありました。6年固定値から、経験年数を引き数とする対応を行いました。

import sc3
import itertools
import math

def 夜勤ペア禁止(list0,list1):
    for day in 全日:
        if  day< 制約開始日:
            continue
        or_list0=[]
        or_list1=[]
        #sc3.print(daydef[day]+"夜勤ペア禁止しました。")
        for person in list0:
            v1=sc3.GetShiftVar(person,day,'入り')#
            sc3.print(staffdef[person]+' ')
            or_list0.append(v1)
        sc3.print('\n')
        for person in list1:
            v1=sc3.GetShiftVar(person,day,'入り')#
            sc3.print(staffdef[person]+' ')
            or_list1.append(v1)
        v1=sc3.Or(or_list0)
        v2=sc3.Or(or_list1)
        sc3.AddSoft(~(v1&v2),"夜勤ペア禁止",5)

        or_list0.clear()
        or_list1.clear()
        
        for person in list0:
            v1=sc3.GetShiftVar(person,day,'明け')#
            or_list0.append(v1)
        for person in list1:
            v1=sc3.GetShiftVar(person,day,'明け')#
            or_list1.append(v1)
        v1=sc3.Or(or_list0)
        v2=sc3.Or(or_list1)
        sc3.AddSoft(~(v1&v2),"夜勤ペア禁止",5)

        sc3.print(daydef[day]+"夜勤ペア禁止しました。\n")
def 休日日勤ペア禁止(list0,list1):
    for day in 今月休診日:
        or_list0=[]
        or_list1=[]
        #sc3.print(daydef[day]+"夜勤ペア禁止しました。")
        for person in list0:
            v1=sc3.GetShiftVar(person,day,'日勤')#
            or_list0.append(v1)
        for person in list1:
            v1=sc3.GetShiftVar(person,day,'日勤')#
            or_list1.append(v1)
        v1=sc3.Or(or_list0)
        v2=sc3.Or(or_list1)
        sc3.AddSoft(~(v1&v2),"休日日勤ペア禁止",5)
        sc3.print(daydef[day]+"休日日勤ペア禁止しました。\n")

def 夜勤人数は最大1人(slist):
    for day in 全日:
        if  day< 制約開始日:
            continue
        list0=[]
        for person in slist:
            v1=sc3.GetShiftVar(person,day,'入り')#
            list0.append(v1)
        sc3.AddSoft(sc3.SeqError(0,1,1,list0),"夜勤人数は最大1人",5)

def 休日日勤人数は最大1人(slist):
    for day in 今月休診日:
        list0=[]
        for person in slist:
            v1=sc3.GetShiftVar(person,day,'日勤')#
            list0.append(v1)
        sc3.AddSoft(sc3.SeqError(0,1,1,list0),"休日日勤人数は最大1人",5)

def 経験年数6年未満の組み合わせを禁止する( years ):
    s=set() #set
    for person in 経験年数属性.keys():
        経験年数=経験年数属性[person]
        s.add(経験年数)
    for 経験年数 in s:
        if 経験年数 * 2 < years:
            slist=[]
            for person in 経験年数属性.keys():
                if 経験年数 == 経験年数属性[person]:
                    slist.append(person)
            夜勤人数は最大1人(slist)
            休日日勤人数は最大1人(slist)
                
    for v in itertools.combinations(s,2):
        if v[0]+v[1]< years:
            list0=[]
            list1=[]
            sc3.print("経験年数"+str(v[0])+"年"+str(v[1])+"年\n")
            for person in 経験年数属性.keys():
                経験年数=経験年数属性[person]
                if  経験年数==v[0]:
                    list0.append(person)
                if  経験年数==v[1]:
                    list1.append(person)
            夜勤ペア禁止(list0,list1)
            休日日勤ペア禁止(list0,list1)

2020年3月27日金曜日

最も多い質問

「解が出た後、(マニュアルで)手直しできないか?」

という質問が最も多いです。「答えは出来ません。」 です。代替は、
「予定入力で入れてしまう」

です。決まっている予定は、勿論ですが、解が出た後で、
「ここは、こうしたい」というシフトを予定として入れてしまいます。
その上で解を求めます。そうすると、それらを肯定した上での最適解が解として出てきます。


2番目に多い質問は、

「この制約は、黄色のマークが出ていて、満足していないのだけれども、予定を見ると空いているので(ご自分自身で)手直しできそうなんだけど?」

答えは
「出来るかもしれませんが、以前より悪い解になります。」
です。
解は、全てのハード制約を満足して、全てのソフトエラー(ソフト制約の逸脱)の重み総和が最小になるように出力しています。もし、その制約を予定で無理やり満足させたとしても、その他のエラーが必ず増加し、以前の解よりは、悪い解(目的関数値が上昇)となります。

黄色マークが出たというのは、最適化した上で出ているということです。出したくはないのだけれども、目的関数値最小という全体としての利益のために、必然の結果として出ているのです。
それを無くすことを優先させるということは、必ず他にしわ寄せが出る、多くの場合、それより上位のソフト制約を満足しないという結果をもたらします。それでもよければ、よいのですが、その場合は、重みが設定が適切でない、ということです。調整してください。

このあたりの質問がでているうちは、まだSC3の能力を未だ信じられない、疑っている 段階です。
(人間とは比べるべくもないのですが。)

使いこなしていくうちに、こういった類の質問は出なくなります。(考えるよりもSC3に任せたほうが早いということが身にしみてくるようです。)


 

2020年3月25日水曜日

操作動画

最近、行っていることとして、操作動画を作っています。
ユーザさまが、やり方が不明なときに、操作をそのままビデオに取って、AVIで上げています。エッセンスだけを編集して格好よくするやり方ではなく、その場で操作をして、コメントをつけているだけですので、冗長で不細工ですが、「ないよりはまし」、ということで敢えて上げています。スタッフ名を匿名にしてなおかつ、ユーザさま所属が判らないようにはしています。メールやTEL等でわかりづらいかな、というときに不定期で作成しています。YouTubeだと画素が荒いので、AVIにしています。使用ソフトは、DemoCreatorです。

2020年3月24日火曜日

制約記述は、何故~禁止 になるか?

制約の書き方として、Pythonも含めてですが、~禁止という書き方が非常に多い ことに気付いていると思います。今日は、何故、そのような書法になってしまうのかを考えたいと思います。

結論的に言うと、「できる限り解空間を狭めないため」、別な言い方をすれば、「決め付けないことにより、可能性を残すため」であります。

例えば、禁止の反対は、強制になりますが、そのような制約は、列制約にしか許していませんし、そもそも「強制」制約を使った記述は、サンプルでもほぼ皆無と言ってもよいと思います。

例えば、そのシフトに強制してしまうと、それ以外は、許されないことになってしまいます。非常に強い制約となり最終的な記述でもあります。このほかの制約は不要である、といったときだけに使用可能です。なにしろ、他を許していないことから、発展性がない、とも言えます。
「強制」 →「これしか許さない」→「他は許さない」
これになにか付加する制約は許されない、ということになります。


一方、禁止記述ですが、
Aは禁止
Bは禁止
....
禁止条項は、空集合にならない限りいくらでも付加していけます。つまりある場面では、

Aは禁止

別な場面では、

Aは禁止
Bは禁止

さらに別な場面では、
Aは禁止
Bは禁止
Cは禁止

というように、汎用性があって柔軟な記述は、禁止により記述していく方法なのです。
これは、制約のANDで記述していることに他なりません。これをCNF(Conjunctive normal form)
と言います。SATで記述する方法そのものです。 (SAT世界でのCNFとDNFは、OR世界でのPrimal-Dual にアナロジーを感じるのは私だけでしょうか)

ところが、

Aに強制


では、他に記述のしようがありません。これ以外に許されないので、発展性のない記述になります。

<メンテナンス>
一つの月で設定した制約記述が未来永劫同じであることはありえません。多かれ少なかれ、
変更が伴うものです。そのとき、Aに強制で、記述してしまっていると、一旦その記述は削除して再度記述し直すことが必要になります。一方、禁止記述の方は、削除でなく追加で済む場合が多いです。メンテナンス性に優れているのは、禁止記述の方です。

<解空間>
よくシフトパターンで、~禁止で記述しますが、強制でも記述可能かもしれません。でも仮にそういう風に記述できたとして、果たして解空間は、どうなってしまうでしょうか?
それ以外を許さないのですから、解空間は大幅に狭まり、容易に解が空集合になってしまうでしょう。理想パターンを一つ決めて、それで解が得られる系であれば、それでもよいです。ですが、その系は、大幅にリソース(人的資源)に余裕がある職場でなけれなりません。さもないと解が空集合、解がないことになってしまいます。NSP問題は、そんな流暢なことは言っていられない、リソーストレードオフのせめぎあい、戦場が一般的です。

~禁止の背景をご理解頂ければ幸いです。



2020年3月23日月曜日

詳細な説明を頂きました。

完全には理解できていないのですが、丁寧な説明を頂きました。

CLPの方が速いのは、IDIOT CRASHという初期基底解構成方法が功を奏しているということらしいです。PhD student のIvet Galabovaさんがthe Idiot crashについて研究されています。
https://www.maths.ed.ac.uk/hall/GaHa18/

お顔は、こちらで拝見することができます。
https://www.maths.ed.ac.uk/hall/HiGHS/#team

他の研究者とシェアしたいとのことでしたので、ご随意にと。そこで
厚かましくも、IPXとSimplex相互にスマートにスイッチする方法について、リクエストしておきました。

このニ三日、久々に英文メールを書きました。Grammarlyがよいです。頼りになります。

2020年3月22日日曜日

データ採取

説明用のWEBページを作りました。少しでも教授のお役にたてばよいのですが。

一応COPTにもアクセスして評価ライセンスをリクエストしておきました。来たら同様の評価を行います。 

2020年3月20日金曜日

COPTって何者?

http://www.orsj.or.jp/archive2/or64-4/or64_4_238.pdf
で線形計画ソルバが紹介されています。この結果は、結構新しいと思うのですが、
言及がありません。これによるとCOPTというソルバがダントツの結果となっています。

どうも中国らしいのですが、学会でも聞いたことがないのでその正体は不明です。ダンツィークが、半世紀以上前に考案したSIMPLEXは、大規模では内点法に代わっていますが、それでもこのパフォーマンスは魅力的です。

HiGHsについて問い合わせをしたら、Julian Hall教授から直接にメールをもらいました。恐れ多いです。貴重なアドバイスを頂いたので、お礼として少しHiGHsにContributionすることにしました。現在SC3でRequestデータを採取中です。Julian教授の業績はこちら 修正Simplex法及びパラレル化で著名です。弟子のHuangfu氏が、FICOXpressに移ったのですね。このころからCLPをOVERTAKEするというプランがあったようです。 

2020年3月19日木曜日

Excel Formatted 出力のサポート

128で、複数のExcelシートに同時に出力できるようにしました。


一番上は、通常の出力で、2番目は、Formatted出力となります。
何が違うかというと、Excelに書いてあるスタッフのところにのみ、データが書かれます。
スタッフ名は、順不同でOKです。解として求めていないスタッフ名があってもよいです。その場合は無視されるだけです。
また、GUI内では、空白等Trimしていますが、長谷川 孝幸 等、空白があってもOKです。
また、色も白黒とすることができます。また、予定制約でハード制約として入力している部分は赤にすることもできます。また、日等 特定のラベルを出力しないことも出来ます。

要するに、元の勤務表に直接出力することを狙っています。

もう一つの機能は、行制約についてのみですが、制約名を書くと、その結果を出力できます。
2番目の画像で、右側3ブロックは、実は同じ項目名なのですが、意図的にずらしています。

最初のブロックは、今月解出力になります。(スタッフ名という項目と同じ行)
SC3GUIと同じ行制約名を発見したので、その下の結果の数字は、SC3が書いているわけです。

2番目3番目のブロックは、先月までの累計と、今月を含めた累計で、同じ項目名なのですが、同じ行に書いてしまうと、SC3がそこに結果を書いてしまい折角の累計が消失してしまうというバグの回避のために、意図的に行をずらしています。

こういう形でExcelで累計処理し、「今月は累計数が少ない部分により多く割り当てる」、という処理をPythonで、動的に書いています。月内ではなく、年度内平準化のためには、どうしても年度内累計処理を行う必要がありました。

この行制約出力機能は、急遽つけました。最初は、Excelでsumif..で書いていたのですが、どうにも複雑で、書いてられない、ということに気付きました。実際、Day集合を簡単に指定できるのでExcelで書く手間よりも、遥かに少ない時間でできました。SC3のC#コード追加変更時間を含めても、少なかった位です。

2020年3月18日水曜日

お客さま仕様から制約仕様を読み取るⅡ



お客さま仕様は、以下です。

TaskA担当は2人。常勤非常勤各1人。TaskAの担当が一人不在の場合 他部署から1人応援に行く 常勤不在の場合は常勤が応援 非常勤不在の場合は非常勤が応援

これより、可能な組み合わせは、下の通りです。◎は、推定
TaskA組み合わせ        
TaskAGr 常勤  
TaskAGr 非常勤  
他部署応援常勤
他部署応援非常勤    

これから、制約は、次の通りとなります。

Σ 常勤==1
Σ非常勤==1
ΣTaskA==2

ポイントは、「全ての組み合わせを列挙して、それに共通する制約を考える」 
です。

 

2020年3月17日火曜日

隔週の記述

TaskAGrは、2人おり常勤と非常勤。
 TaskA土曜日勤は1人。TaskAGrが隔週担当、その他は他部署が担当(年度内平準化)

という仕様をどう実現するかという問題です。
  
 TaskA常勤土曜日 a)    ○                  ○
 TaskA非常勤土曜日b)            ○                 ○
 他部門勤務日    c)                   △         △

隔週ということに拘ると、制約で記述するのが難しくなります。制約で記述することも可能ですが、大体こういうのは、ルール上の例外が多々あって、周期がズレル結果を生むことが多いのです。なので、隔週自体をユーザさまに決めてもらうのが得策です。

つまり、TaskA常勤・非常勤勤務日をカレンダで指定してしまいます。 そうすると他部門の勤務日は、a)でもb)でもない、今月土曜日という、Day集合演算で求まります。 

2020年3月16日月曜日

月の後半から夜勤開始

新人の配属月です。早番や夜勤など、月の後半から行うようにしたいというのは、どのように記述したらよいでしょうか? 考えられる方法は、以下の二つです。

1)夜勤・早番以外というラベルを作って、月前半を埋めてしまう
2)スタッフプロパティで夜勤禁止期間1、早番禁止期間1という属性を作って新人にそれを設定する。禁止期間(月前半)が終われば、通常勤務となります。夜勤開始→夜勤禁止とするところが、汎用性の高い記述とするポイントです。禁止期間が終われば、通常勤務となり次月以降もメンテナンスする必要がありません。

いずれも、今月だけになりますが、来年もまた使えるので作っておくことは意味があります。

今回は、2)の方法での記述です。




2020年3月15日日曜日

お客さま仕様から制約仕様を読み取る

お客さま仕様

2人の仕事ペアで、夜勤及び休日日勤を行う。
組み合わせは 主任&常勤、主任&非常勤、副主任&常勤、副主任&非常勤、常勤&常勤、常勤&非常勤、
非常勤&非常勤はできる限り避ける

これを表にすると以下のようになります。
 
  主任 副主任 常勤 非常勤
主任    
副主任  
常勤
非常勤


空欄は、記載されていないのですが、xと解釈するべきでしょう。
そうすると、

■主任は、2人不可
■副主任も2人不可
■非常勤も2人不可

2人不可 → 1人MAXです。
これをまとめて、以下のような制約を実装しました。 


 

2020年3月14日土曜日

勤務数の年度内平準化

月内平準化は、SC3内で単独処理が出来ますが、年度平準化は、出来ません。そこで、Excelを使用して、年度内の平準化を試みます。

下は、スタッフプロパティで、土日祭日の日勤と
夜勤数の累計を読み込んでいます。今回初期値として0を入れました。

累計数が多いスタッフは、なるべくその勤務をさせたくない、という風に考えます。
そうすると、累計数に応じてその勤務をさせたくないというソフトレベルを上げていけばよい、ということになります。

以下がそのソースです。実装上のポイントは2つあります。

<ソフト制約を書く場合のポイント>
1)できるだけソフト制約を使わない
2)ソフト制約同士のコンフリクトを避ける

ソフト制約を使えば使うほど、重くなります。なので、使わずに済めば それに越したことはありません。

2番めのポイントは、ソフト制約同士のコンフリクトを避ける、ということです。コンフリクトがあるかないかは、Default状態で、エラーの有無で確認することが出来ます。コンフリクトがあると、通常状態もしくは、予定が白紙状態でもエラーカウントがされる状態となります。このような状態であると、なにがしかの性能上の劣化をもたらします。なので、Default状態では、エラーカウントがされない、ということを目指してください。0にすることは難しいかもしれませんが、Defaultでのエラー数を少なくすることは、意味があります。

さて、実装に戻りますが、一番下の累計からアサインされるようにしたいのですが、1)の原則より、
一番下では、ソフト制約化しません。2番目の累計数から順次ソフトレベルを上げる実装としています。また、既にハード予定入力がされている場合は、1)原則により、ソフト制約化しません。ハード制約の場合、shift_schedules[person][day][0]はブランクではなく、かつshift_schedules[person][day][1]が0になっています。

最後に、SC3GUIでの問題なのですが、ダイナミックにソフトレベルを変更するのに、
sc3.AddSoft(~v1,s,soft_level)
とは書いていません。ここのsoft_levelは、定数を指定する必要があります。GUIは、ここの定数を見て、求解画面の適用チェックを出しています。GUIはそれほど賢くないので、定数で書いていないと、ソフト制約が無視されてしまうことになります。

後は、当月の当該のカウントをExcelに出力して、累計との加算処理をExcelで行えばよいです。
 この辺の処理は、ユーザさまにお願いすることになりますが、 求解を何度も行うのが、普通なので単純な実装とすることはできません。マニュアルでお願いしたほうがよいかもしれません。





def write_not_preferred(person,day,not_preferred_shift,level):
    v1=sc3.GetShiftVar(person,day,not_preferred_shift)
    s=staffdef[person]+daydef[day]+' '+not_preferred_shift+'は、好ましくない ソフトレベル='+str(level)
    if level==1:
        sc3.AddSoft(~v1,s,1)
    elif level==2:
        sc3.AddSoft(~v1,s,2)
    elif level==3:
        sc3.AddSoft(~v1,s,3)
    elif level==4:
        sc3.AddSoft(~v1,s,4)
    else:
        sc3.print("Unsupported level ")
    sc3.print(s+'を行いました\n')

def not_preferred(day_list,person,not_preferred_shift,level):
    for day in day_list:
        ts=shift_schedules[person][day][0]
        if ts =='':
            write_not_preferred(person,day,not_preferred_shift,level)
        elif shift_schedules[person][day][1] !=0:#ソフト制約なら適用
            write_not_preferred(person,day,not_preferred_shift,level)

            
def 累計処理Sub(累計属性,禁止persons1,禁止persons2,daylist,not_preferred_shift):
    #休日日勤不可 夜勤不可者を除いて累計のsetをつくり、少ない順にレベルを上げる
    s=set()
    for person in 累計属性.keys():
        if person in 夜勤なし:
            continue
        if person in 休日日勤不可:
            continue
        累計=累計属性[person]
        s.add(累計)
    level=0
    for 累計 in s:
        for person in 全スタッフ:
            if person in 禁止persons1:
                continue
            if person in 禁止persons2:
                continue
            if 累計==累計属性[person]:
                if level==0:#最小累計は、制約にしない
                    continue
                not_preferred(daylist,person,not_preferred_shift,level) #累計が多くなるほど、そのシフトを行うのは嫌だ
        level+=1            

def 累計処理():
    
    累計処理Sub(土日祭日の日勤回数累計属性,休日日勤不可,休日日勤不可,今月休診日,'日勤')
    累計処理Sub(土日祭日の夜勤回数累計属性,休日日勤不可,夜勤なし,今月休診日,'夜勤')
/pre>

2020年3月13日金曜日

公休取得は、夜勤を行った日の前後3日以内


お客さまの要求仕様は以下です。

金土、夜勤を行った者は公休1日取得
土日、夜勤を行った者は公休2日取得
日月、夜勤を行った者は公休1日取得
祭日、夜勤を行った者は公休なし
土日祭日、日勤を行った者は公休なし
公休取得は、夜勤を行った日の前後3日以内

ここでいう公休は、代休といった方がイメージが合うかもしれません。
で、実装を考えました。

制約開始日制約終了日
代休禁止 代休カウント期間入り明け 代休カウント期間代休数1代休禁止 
 代休カウント期間入り明け 代休カウント期間代級数2
 代休カウント期間入り明け代休カウント期間代級数1
夜勤カウント期間

SeqCompで、「夜勤カウント期間における夜勤カウントと代休期間における代休カウントが等しい」
と制約すればよいことが分かります。

なお、3日間は、前が水木金、後が月火水固定、ただし、前月や次月に跨ることは不可、という仕様です。

実装は、下のPythonソースです。2週連続夜勤が発生すると誤動作するのでGUIで7日以内の夜勤連続を禁止しています。また、別に、制約開始日付近と制約終了日付近で、夜勤カウント区間がないときは、代休を禁止する処理も行っています。


def 土日夜勤した場合前後3日間で公休を取る():
    for day in 祝でない今月土:
        if day > 制約開始日:#Always True
            dstart=day-3
            holiday_domain=[]
            sat_sun=[]
            for i in range(8):#前3日 土日 後3日 Total8days
                if dstart+i <制約開始日 or dstart+i>制約終了日:
                    continue
                if dstart+i =day+2:#後ならば
                    if dstart+i in 休診日:
                        continue
                    holiday_domain.append(dstart+i)
            #必要な期間を得たので夜勤可能者について制約する
            
            for person in 夜勤可能:
                
                vsat_sun=[]
                vholidays=[]
                for day in sat_sun:
                    sc3.print('土'+str(day)+'\n')
                    v1=sc3.GetShiftVar(person,day,'入り')#
                    v2=sc3.GetShiftVar(person,day,'明け')
                    vsat_sun.append(v1|v2)
                for day in holiday_domain:
                    sc3.print('H'+str(day))
                    v1=sc3.GetShiftVar(person,day,'公休')#
                    vholidays.append(v1)
                s=staffdef[person]+daydef[day]+'代休処理'
                sc3.AddSoft(sc3.SeqComp(vsat_sun,vholidays),s,7)

2020年3月12日木曜日

経験年数6年以上の組み合わせのみを許す

スタッフプロパティで、経験年数を記述して、合計が6年以上のときだけ、夜勤や休日日勤を許す記述です。次がその実装です。最初にset()で、経験年数の集合を割り出しています。次にpython itertoolsで、全ての組み合わせを求めて、合計が6未満の組み合わせを禁止にしています。
注意するのは、同じ年数の場合もあるので、x2して<6ならば、という処理も必要となります。(2人で6年未満なら、その年数の総和は、1人以下となるようにして、2人は勤務させないようにしています。)

全スタッフについて、itertoolsで廻すのではなく、setを用いて組み合わせ爆発を抑制する記述としているところがエッセンスになります。

経験年数の合計が6年以上というのは、新しい発想ですが、実用的ではないでしょうか?
ユーザさまの要求仕様に教えられることも多いです。今後も、そのような記述上のノウハウを共有していきたいと思います。

def 経験年数6年未満の組み合わせを禁止する():
    s=set() #set
    for person in 経験年数属性.keys():
        経験年数=経験年数属性[person]
        s.add(経験年数)
    for 経験年数 in s:
        if 経験年数 * 2 < 6:#Self組み合わせ の総数和<=1に制約する
            slist=[]
            for person in 経験年数属性.keys():
                if 経験年数 == 経験年数属性[person]:
                    slist.append(person)
            夜勤人数は最大1人(slist)
            休日日勤人数は最大1人(slist)
                
    for v in itertools.combinations(s,2):
        if v[0]+v[1] < 6:#この組み合わせを禁止する
            list0=[]
            list1=[]
            sc3.print(str(v[0]),str(v[1]))
            for person in 経験年数属性.keys():
                経験年数=経験年数属性[person]
                if  経験年数==v[0]:
                    list0.append(person)
                if  経験年数==v[1]:
                    list1.append(person)
            夜勤ペア禁止(list0,list1)
            休日日勤ペア禁止(list0,list1)


2020年3月11日水曜日

127Cは使用しないでください

大変申し訳ありません。
予定入力が入ると基数制約(不等式制約)が誤動作するので使用しないでください。