前回・前前回のまとめになります。
時に、理不尽なシフトは、何十年経っても覚えているものですから。
前回・前前回のまとめになります。
Ans.はい、可能です。
現状、下のようになっているとします。
今回のまとめは、以下のようになりました。
Ans. 申し訳ございません。Python記述にバグがありました。
<変更前>
def get_sheet_name(): s=daydef[制約開始日] year_str=s[0:4] month=int(s[5:7]) sheet_name=year_str+'年'+str(month)+'月' print("シート名=",sheet_name) return sheet_name def get_year(): s=daydef[制約開始日] year_str=s[0:4] return int(year_str) def get_year_str():#DEC202024 追加 s=daydef[制約開始日] year_str=s[0:4] return year_str def get_month(): s=daydef[制約開始日] month=int(s[5:7]) return month def get_day(): s=daydef[制約開始日] day=int(s[8:10]) return day def find_address(moncal,D): row=0 for r in moncal: if D not in r: row+=1 continue return (row,r.index(D)) #次月最初の日:最終行の最終列+1を出す tuple=find_address(moncal,D-14) row=tuple[0]+2 col=tuple[1] print("find_address",row,col) return (row,col) raise IndexError def init_row_columns(): global start_row global start_column global row_interval global column_interval start_row=6 start_column=2 row_interval=6 column_interval=2 def findcell(ws,moncal,D): tuple=find_address(moncal,D) init_row_columns() row=start_row+row_interval*tuple[0] column=start_column+column_interval*tuple[1] c=namedtuple("Row","Column") c.Row=row c.Column=column return c def clear_cells(ws): init_row_columns() for r in range(6): for d in range(7): row=start_row+r*row_interval column=start_column+d*column_interval print(row,column) ws.Cells(row ,column+1).Value="" ws.Cells(row ,column).Value="" ws.Cells(row+1,column+1).Value="" ws.Cells(row+1,column).Value="" ws.Cells(row+2,column+1).Value="" ws.Cells(row+2,column).Value="" ws.Cells(row+3,column).Value="" ws.Cells(row+4,column).Value="" def post_main(): print('\n\n*********ポスト処理を実行中です。*************\n') import win32com.client#pywin32をインポート #すでにExcelが起動されている場合はそのタスクが使われる #エラー終了するとタスクは残ります try : xl = win32com.client.Dispatch("Excel.Application") except: print("can not invoke excel") exit() #動いている様子を見てみる xl.Visible = False #file=get_open_file_name("Open Excel File") #print(file) os.chdir(project_file_path) filename=get_year_str()+"当直拘束表.xlsx";##DEC202024 BUG FIX "2024当直拘束表.xlsx" file=os.path.join(project_file_path,filename) file1=file.replace("/","\\") #なぜか逆スラッシュでないと動かない print(file1) wb = xl.Workbooks.Open(file1) # Excelシートオブジェクト ws = wb.Worksheets(get_sheet_name()) # 指定したシートを選択 # Select()の使用前にシートのActivate()が必要 ws.Activate() calendar.setfirstweekday(6)#日曜日を最初に mon_cal=calendar.monthcalendar(get_year(),get_month())#行列でリスト print(findcell(ws,mon_cal,1)) clear_cells(ws) for day in 今月区間: D=day-制約開始日+1 print(D) c=findcell(ws,mon_cal,D) n=findcell(ws,mon_cal,D+1) #print(c.Row,c.Column) day_solution_analysis(ws,c,n,day) wb.Close(True)# Trueで保存。False=Defaultでブックを保存せずにクローズ # Excel終了 xl.Quit() print('\n\n*********ポスト処理を実行終了しました。*************\n')
<変更後記述>
def get_sheet_name(): s=daydef[制約開始日] year_str=s[0:4] month=int(s[5:7]) sheet_name=year_str+'年'+str(month)+'月' print("シート名=",sheet_name) return sheet_name def get_year(): s=daydef[制約開始日] year_str=s[0:4] return int(year_str) def get_year_str():#DEC202024 追加 s=daydef[制約開始日] year_str=s[0:4] return year_str def get_month(): s=daydef[制約開始日] month=int(s[5:7]) return month def get_day(): s=daydef[制約開始日] day=int(s[8:10]) return day def find_address(moncal,D): row=0 for r in moncal: if D not in r: row+=1 continue return (row,r.index(D)) #次月最初の日:最終行の最終列+1を出す tuple=find_address(moncal,D-14) row=tuple[0]+2 col=tuple[1] print("find_address",row,col) return (row,col) raise IndexError def init_row_columns(ws):#DEC272024 global start_row global start_column global row_interval global column_interval test_range= ws.Range("A1:C9")#DEC272024 ### example if you want to find out the column of search result #ResultColumn= test_range.Find("Series ID").Column #print(str(ResultColumn)) sun=test_range.Find("日")#DEC print("日アドレス=",sun.Row,sun.Column) start_row=sun.Row+2# 6 start_column=sun.Column#2 row_interval=6 column_interval=2 def findcell(ws,moncal,D): tuple=find_address(moncal,D) init_row_columns(ws) row=start_row+row_interval*tuple[0] column=start_column+column_interval*tuple[1] c=namedtuple("Row","Column") c.Row=row c.Column=column return c def clear_cells(ws): init_row_columns(ws) for r in range(6): for d in range(7): row=start_row+r*row_interval column=start_column+d*column_interval print(row,column) ws.Cells(row ,column+1).Value="" ws.Cells(row ,column).Value="" ws.Cells(row+1,column+1).Value="" ws.Cells(row+1,column).Value="" ws.Cells(row+2,column+1).Value="" ws.Cells(row+2,column).Value="" ws.Cells(row+3,column).Value="" ws.Cells(row+4,column).Value="" def post_main(): print('\n\n*********ポスト処理を実行中です。*************\n') import win32com.client#pywin32をインポート #すでにExcelが起動されている場合はそのタスクが使われる #エラー終了するとタスクは残ります try : xl = win32com.client.Dispatch("Excel.Application") except: print("can not invoke excel") exit() #動いている様子を見てみる xl.Visible = False #file=get_open_file_name("Open Excel File") #print(file) os.chdir(project_file_path) filename=get_year_str()+"当直拘束表.xlsx";##DEC202024 BUG FIX "2024当直拘束表.xlsx" file=os.path.join(project_file_path,filename) file1=file.replace("/","\\") #なぜか逆スラッシュでないと動かない print(file1) wb = xl.Workbooks.Open(file1) # Excelシートオブジェクト ws = wb.Worksheets(get_sheet_name()) # 指定したシートを選択 # Select()の使用前にシートのActivate()が必要 ws.Activate() calendar.setfirstweekday(6)#日曜日を最初に mon_cal=calendar.monthcalendar(get_year(),get_month())#行列でリスト print(findcell(ws,mon_cal,1)) clear_cells(ws) for day in 今月区間: #D=day-制約開始日+1 D=day-制約開始日+get_day()#DEC272024 BUG FIX #print(D) c=findcell(ws,mon_cal,D) n=findcell(ws,mon_cal,D+1) print("Day=",D,c.Row,c.Column) day_solution_analysis(ws,c,n,day) wb.Close(True)# Trueで保存。False=Defaultでブックを保存せずにクローズ # Excel終了 xl.Quit() print('\n\n*********ポスト処理を実行終了しました。*************\n')
mainのDの記述を暦日に修正しました。また、init_row_columnsで、日曜日の「日」を検索・検出してそれを基準に
Excelデータを記述するように変更しました。
Q.具体的には、【仮セット版】宿日直パンフレットにも記載されていますが、
1 宿直勤務は週1回まで
2 日直勤務は月1回まで
の制約を、満たす必要があります。
2については、スタッフ定義の宿日直回数属性を修正すれば良いような気がしますが、1については制約の追加が必要になるのではないかと思います。一週間の定義は、日曜日をスタートとする一週間でお願いします。
Ans.
一週間の定義が難しいと思います。日曜日をスタートとする一週間の実装が過去にないか?を検索します。
本ブログで、「週あたり」で検索します。
Q.貴社が提供されています「スケジュールナース」の導入も検討している次第です。ホームページ等で内容を確認致しましたが、一度システムに関して詳細をお聞きしたいと考えております。Zoom等でのご説明でも問題ありません。ご対応可能でしょうか。
Ans.
ご照会ありがとうございます。ご希望の仕様を簡易実装した上で、デモしながら説明するのが、分かり易いと思います。
簡単でよいので仕様をお送りいただければ幸いです。ZOOMのご希望日時を、ご連絡頂けますでしょうか?こちらは、25日以降、来月15日以前は、大体空いています。
ご検討の程よろしくお願いいたします。
Q.下記の変更をお願い出来ませんか?
①列や行に看護師の人数の変更。
②外来看護師の人数をいれない
Ans.
ZOOM上で修正を承りたいと思います。
ご都合の良い日時をお知らせください。毎月15-25日は、サポートで混むので、出来れば、その区間を外して頂けると助かります。
Ans.
赤字で表示されているので解がありません。
●平日休みの数が7名を超えない金
をダブルクリックすると当該制約箇所が表示されます。
当該行の曜日タイプ部でマウスミドルボタンを押すと曜日集合が表示されます。
制約を見ると、
1月3日の休みの数が最大で8名と制約しています。
ソフト制約レベル6です。
列制約レベル6の許容エラーは、現在3になっています。
解は出ました。12人が休みになっていることが分かります。
制約最大8人に対して、実際は、12人ですから、許容エラーは、
12-8=4
4以上である必要があります。許容エラー3の場合、変更前ソフトエラー範囲は、
8プラス3ですから、11人までがソフトエラー範囲つまりハード制約で、12人以上はハード制約を超えるのでエラーとなります。ですから12人の場合は「解がない」エラーとなってしまいます。
これで、エラーの理由が判明しました。1月3日に大量の休みがエントリーされているということです。
ところで、制約の曜日集合名を見ると、
「祝ではない金今月」
になっています。1月3日は、確かに金曜日ですが、祝ではない..?
確かに、祝ではないですが、年始ではあります。そこで、曜日集合の定義を見てみます。
Q 具体的には、
13日 xx先生 拘束 14日宿直
15日 xx先生 拘束 16日宿直
を解消できればと思います。
Ans.
以下の制約を追加しました。
ベンチマーク問題INRC2 n110w4_0_1-4-2-8を解く途中に出現するSAT問題を用いて性能比較しました。
ヒントの数が多ければ多いほど解くのが容易となる傾向にあります。ヒントのサイズ、Fixed Lits Sizeをパラメータとしたとき、何秒で解けるかを評価したものです。
ソルバは、CADICAL最新版、KISSAT去年、KISSAT今年、DPS去年、PRS今年のSAT competition提出版になります。DPSとPRSは、32threadsのパラレル版で、いずれもその時点のKISSAT最新版がベースになっています。
青が1位、黄色が2位でマークしています。一見して分かるのは、
■パラレル版が10倍程度高速で安定傾向、PRSが最速
■SAT Solver Single thread版でも最新になるほど性能向上が見られる
さらに考察として、
PRSに搭載されているKISSATは、去年以前のKISSATがベースですから、今年のKISSATにReplaceすれば、さらに性能向上が期待できます。
しかし、ヒントの数が少ないと、PRSをもってしても現実時間内に解くことは出来ません。(1割少なくなっただけで、もはや解くことが出来ません。ヒントなしでは論外。)
つまり、SATソルバだけでは解くことは出来ないことを示しています。一般に、解空間が広い場合(最適化の結果、残る解の個数が多い場合)SATソルバのみで解くことが出来ますが、INRC2問題のように解空間が非常に狭い最適化問題の場合は、複合技が必須となります。今回の問題で言えば、SATソルバを選ぶことに注力するより、ヒントの数を増やした方が遥かに効率的に解くことが出来ます。
今回、パラレル版で意外な性能向上が見られたので、採用しようかな、と思います。今回求めているのは、UNSAT証明能力で、SAT解探索能力と比例傾向にあるからです。
学生さんの発表会です。ZOOMで視聴することにしました。
検査技師のシフト勤務表のモデリングについて興味深く拝聴しました。スケジュールナースでも、実装例があります。
(確か、同じ神戸大学にも数年前に納入していたと思います。)
OR学会の会員ではありませんが、将来、こちらでもスケジュールナースの最近の成果についても、報告できれば、と考えています。
<特殊から一般へ>
ナーススケジューリング問題のオープンインスタンスを全てCloseするという空前絶後の壮大な計画は、難航していますが、INRC2については、形が見えつつあります。今までに得られた知見を総合すると、ナーススケジューリング問題を解く一般系というのが、少し見えてきたような気がします。特殊で困難な問題群ではありますが、それを解く努力を重ねることで、それで得た知見を基に、実務上の比較的容易/困難なインスタンス群についても、最適解をリーズナブルな時間内に提示するという、
塾講師配置問題の最適化 #数理最適化 - Qiitaの「ナーススケジューリング問題は、終わっていない」
に対する回答を示せるのではないか?と考え始めています。当初は、ただ世界記録更新だけを目的にしていたのですが、特殊なインスタンスから一般インスタンスへの応用が可能ではないか?と気づきました。言い換えると特殊解法から、一般解法への統一化です。どのような問題も、それに適した特殊なアルゴリズムを適用するのではなく、一般化した一つのアルゴリズムで解くのがスマートであることは言うまでもありません。どのような問題にせよ、それに入力すれば、最適解がリーズナブル時間内で出力される、それがあればアルゴリズムを選定する必要はありません。
現状のナーススケジューリング問題は、二つの課題があります。
<最高性能のソルバ開発>
実務のナーススケジューリング問題は、Gurobiを持ってしても厳密解をリーズナブルな時間内に解くことは難しい場合が多いというのが現状です。リーズナブルな時間内に厳密解を提示できることが、理想なことは言うまでもありません。現在でも、スケジュールナースは、世界最高性能を持つと自負しておりますが、上記課題を達成する最初の製品となるべく日夜邁進中です。
<モデリング、誰が行うのか>
もう一つの現実課題は、モデリングです。勤務表は、作って終わりということはなく、月々にアップデートしていく必要があります。時に、制約追加ということがあります。つまりメンテナンスを誰が行うか?ということが常に付きまといます。モデリングのAI化、ハード制約やソフト制約等基本となる教育も含めて、両面から考えていく必要があります。
以上二つの課題にたいして、取り組む所存です。
Ans.申し訳ございません。
貴病院用医師当直拘束表のPython記述が2024年の固定となっておりました。Python記述のバグです。申し訳ございません。
以下のようにPython記述を修正しました。
<修正前>
<修正後>
Pythonソース修正
貴病院用ユーザマニュアル追加