【テクニカル・上級編】【プロ向け】制約条件の「強制解除」によるスケジュール最適化とボトルネック解消 – Project VBA解析バイブル

スポンサーリンク
スポンサーリンク

制約の枷を解き放て:スケジュール最適化エンジンの設計思想と「強制解除」の極致

多くのプロジェクトマネージャーや、それらを支援するシステム開発者は、Microsoft Projectの「制約条件(Constraints)」を、計画を保護するための「盾」だと誤解している。

しかし、我々のようなアーキテクトの視点は異なる。厳格すぎる制約条件――`Must Start On` や `Finish No Later Than` といった「ハード・コンストレイント」は、スケジュールの流動性を奪い、クリティカルパスの計算を歪める「枷(かせ)」に他ならない。これらが複雑に絡み合ったとき、プロジェクトは真のクリティカルパスを見失い、不必要なバッファを抱え込み、リソースの最適配分を阻害するボトルネックへと変貌する。

本稿では、VBAを用いてこれらの「制約による停滞」を検出し、数学的・ヒューリスティックなアプローチで制約を緩和(Relaxation)し、プロジェクト全体の期間を最短化する「制約緩和最適化エンジン」の設計思想について、その実装の深淵までを解説する。

1. 制約条件が引き起こす「偽のクリティカルパス」

MS Projectの計算エンジン(CPM: Critical Path Method)は、タスク間の論理関係(FS, SS, FF, SF)に基づいてスケジュールを計算する。しかし、ここに「日付制約」が介入すると、論理関係とは無関係な「日付の壁」が生成される。

例えば、タスクBがタスクAの終了後に開始される設定(FS関係)であっても、タスクBに「Must Start On(必ずこの日に開始)」という制約がかかっていれば、タスクAがどれだけ早く終わろうとも、タスクBは動けない。このとき、プロジェクトの終端(Project Finish)を決定しているのは論理的な依存関係ではなく、単なる「固定された日付」である。

これを放置することは、エンジニアリングにおける「最適化の放棄」を意味する。

2. 最適化エンジンのアーキテクチャ:Scanner / Evaluator / Reliever

単にすべての制約を消去すればよいわけではない。それは計画の崩壊を招く。我々が構築すべきは、「制約がスケジュールの短縮を阻害しているか」を判定し、安全な範囲で緩和するインテリジェントなエンジンである。

設計は以下の3層構造で行う。

1. Scanner (検知層):
`ConstraintType` を走査し、`pjConstraintAsSoonAsPossible`(最短開始)以外の制約を持つタスクを抽出する。
2. Evaluator (評価層):
「その制約を解除した場合、プロジェクト全体の終了日が前倒しになるか?」をシミュレーションする。
3. Reliever (実行層):
緩和の決定を下し、制約を `pjConstraintAsSoonAsPossible` へと書き換える。

3. 実装の極致:VBAによる高効率最適化ロジック

大量のタスク(数千〜数万規模)を扱う場合、単純なループと `Application.Calculate` の繰り返しは、計算コストの爆発を招き、メモリリークやアプリケーションのハングアップを引き起こす。

以下のコードは、メモリ管理と計算効率を極限まで意識した、制約緩和エンジンのコア実装例である。

Option Explicit

' --------------------------------------------------------------------------------
' Module: OptimizationEngine
' Description: 制約条件を解析し、スケジュールを最適化するプロフェッショナル向けモジュール
' --------------------------------------------------------------------------------

Public Sub OptimizeScheduleByRelaxingConstraints()
    Dim pjApp As MSProject.Application
    Set pjApp = MSProject.Application
    
    Dim targetProject As MSProject.Project
    Set targetProject = pjApp.ActiveProject
    
    Dim t As MSProject.Task
    Dim originalFinishDate As MSProject.Date
    Dim potentialFinishDate As MSProject.Date
    Dim constraintImpactCount As Long
    
    ' パフォーマンス向上のための設定
    ' 計算モードを「手動」に切り替え、ループ内での再計算コストを最小化する
    pjApp.Calculation = pjManual
    
    On Error GoTo ErrorHandler
    
    ' 1. 現在のプロジェクト終了日を記録
    originalFinishDate = targetProject.Finish
    constraintImpactCount = 0
    
    Debug.Print "Optimization Started: " & Now
    
    ' 2. タスクの走査
    ' 大規模プロジェクトでは、タスクのインデックスによるアクセスが最も低負荷
    For Each t In targetProject.Tasks
        ' 終了したタスクや、存在しないタスクはスキップ
        If Not t Is Nothing Then
            If Not t.ExternalTask And Not t.Summary Then
                
                ' 制約条件が「最短開始」以外、かつ完了していないタスクを対象とする
                If t.ConstraintType <> pjConstraintAsSoonAsPossible And Not t.Completed Then
                    
                    ' --- シミュレーションフェーズ ---
                    ' 現在の制約を一時的に退避(メモリ上に保持)
                    Dim originalConstraint As MSProject.ConstraintType
                    originalConstraint = t.ConstraintType
                    
                    ' 制約を「最短開始」に緩和
                    t.ConstraintType = pjConstraintAsSoonAsPossible
                    
                    ' 再計算を実行
                    pjApp.Calculate
                    
                    ' 緩和後のプロジェクト終了日を確認
                    potentialFinishDate = targetProject.Finish
                    
                    ' 判定: プロジェクト終了日が前倒しになったか?
                    If potentialFinishDate < originalFinishDate Then
                        ' 成功:制約を緩和したままにする
                        constraintImpactCount = constraintImpactCount + 1
                        originalFinishDate = potentialFinishDate ' 次のタスク比較のために更新
                        Debug.Print "Constraint Relaxed: Task [" & t.Name & "] -> New Finish: " & potentialFinishDate
                    Else
                        ' 失敗:制約がスケジュール短縮に寄与していないため、元に戻す
                        t.ConstraintType = originalConstraint
                        pjApp.Calculate
                    End If
                    
                End If
            End If
        End If
    Next t

    ' 3. 後処理
    pjApp.Calculation = pjAutomatic
    MsgBox "最適化完了。" & vbCrLf & _
           "緩和された制約数: " & constraintImpactCount & vbCrLf & _
           "新終了日: " & targetProject.Finish, vbInformation

CleanUp:
    ' オブジェクトの明示的解放(COM参照カウンタの適切な管理)
    ' 大規模ループ内ではこれを行わないとメモリが枯渇する
    Set targetProject = Nothing
    Set pjApp = Nothing
    Exit Sub

ErrorHandler:
    MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical
    Resume CleanUp
End Sub

アーキテクトの視点:コードの深層解説

# ① `pjManual` による計算制御

`Application.Calculate` は非常に重い処理である。ループ内で自動計算が走ると、プロジェクトの規模に比例して実行時間は指数関数的に増大する。`pjManual` に設定し、必要なタイミングでのみ `Calculate` を明示的に呼び出すのは、プロフェッショナルな制御の鉄則である。

# ② メモリ管理とCOMオブジェクトのライフサイクル

VBAにおいて、`Set t = Nothing` を行うことは、COMオブジェクトの参照カウンタを減らすことを意味する。大規模なプロジェクトでは、タスクオブジェクトの参照が適切に管理されないと、MS Projectのメモリ使用量が膨れ上がり、最終的に `Out of Memory` でクラッシュする。本コードでは、`For Each` ループの構造を最適化し、不要なオブジェクト保持を避けている。

# ③ ヒューリスティックな判断ロジック

このアルゴリズムは「プロジェクト全体の終了日」を評価指標(Objective Function)としている。個別のタスクの期間が延びたとしても、プロジェクト全体のFinishが早まるのであれば、その制約解除は「正解」である。これは、局所最適解ではなく、全体最適解を求めるための基本的なアプローチである。

4. Windows API を用いた堅牢性の向上(レガシー・システム連携)

もし、この最適化エンジンを外部の .NET アプリケーションや C++ で構築されたシステムから呼び出す場合、あるいは MS Project がハングアップした際の「ウォッチドッグ(監視)」機能を実装する場合、Windows API の活用が必要になる。

例えば、最適化処理が長時間(例:30分以上)停止している場合に、プロセスを安全に強制終了させる、あるいは実行中のスレッドの状態を監視するために `GetTickCount` や `TerminateProcess` を検討する設計が必要になる。

' Windows API を用いた実行時間の計測例 (VBA)

If VBA7 Then

Declare PtrSafe Function GetTickCount Lib "kernel32" () As Long

Else

Declare Function GetTickCount Lib "kernel32" () As Long

End If

' ... ループ内で一定時間経過を監視するロジックを組み込むことで、 ' 無限ループや計算のデッドロックを回避する。

5. 結論:制約を制御下に置け

プロジェクトにおける「制約」は、管理者が意図的にコントロールすべき変数であって、受動的に受け入れるべきルールではない。

VBAを用いた自動最適化エンジンの設計思想の核心は、「制約の価値を数学的に評価し、無意味な枷を自動的に排除する」ことにある。このアプローチをマスターしたエンジニアだけが、カオスなスケジュールの中から、真に実行可能なクリティカルパスを導き出すことができる。

技術の真髄は、ツールを使うことではなく、ツールの背後にある計算ロジックを支配することにある。

タイトルとURLをコピーしました