Delphiの行列とベクトルのクラス

この記事では、線形代数のオブジェクト(ベクトル、行列、四元数)を操作するための型の設計について説明します。 標準操作のオーバーロードのメカニズムの古典的なアプリケーション、「コピーオンライト」技術と注釈の使用が示されています。



数学モデリングの分野で作業する場合、行列、ベクトル、四元数の演算を使用する計算アルゴリズムを扱う必要があります。 驚くべきことに、現代の開発環境の能力にもかかわらず、仲間のプログラマーはしばしばそのような問題を解決するために手続き型のアプローチを使用していることがわかりました。 そのため、行列とベクトルの積を計算するために、次のようなタイプと関数について説明します。



TVec3 = array[1..3] of Extended; TMatrix3x3 = array[1..3, 1..3] of Extended; function MVMult(M: TMatrix3x3; V: TVec3): TVec3;
      
      





オブジェクトアプローチを使用することを提案します。これは、データとその処理方法の共同配置を意味します。 オブジェクト形式で同様のクラスのタスクを解決するためにDelphiが提供する機会を見てみましょう。 オブジェクト構造を設計するとき、次の要件から進みます。





設計



便宜上、補助タイプを定義します。



 TAbstractVector = array of Extended; TAbstractMatrix = array of array of Extended;
      
      





次に、クォータニオン、ベクトル、および行列の構造を定義します。



 TQuaternion = record private FData: array[0..3] of Extended; procedure SetElement(Index: Byte; Value: Extended); function GetElement(Index: Byte): Extended; public property Element[Index: Byte]: Extended read GetElement write SetElement; default; end; TVector = record private FData: TAbstractVector; FCount: Word; procedure SetElement(Index: Word; Value: Extended); function GetElement(Index: Word): Extended; public constructor Create(ElementsCount: Word); property Count: Word read FCount; property Elements[Index: Word]: Extended read GetElement write SetElement; default; end; TMatrix = record private FData: TAbstractMatrix; FRowsCount: Word; FColsCount: Word; procedure SetElement(Row, Col: Word; Value: Extended); function GetElement(Row, Col: Word): Extended; public constructor Create(RowsCount, ColsCount: Word); property RowCount: Word read FRowsCount; property ColCount: Word read FColsCount; property Elements[Row, Col: Word]: Extended read GetElement write SetElement; default; end;
      
      





レコードを使用するのは、 Delphiのクラスコンストラクトに対する操作のオーバーロードは許可されていません。 さらに、レコードオブジェクトには有用なプロパティがあります。データは宣言の場所でメモリ内で展開されます。つまり、レコードオブジェクトは動的メモリ内のインスタンスへの参照ではありません。

ただし、この場合、ベクトルと行列の要素は動的配列に格納され、そのオブジェクトはリンクです。 したがって、明示的なコンストラクターを使用すると便利です。 内部フィールドを初期化し、必要な数の要素にメモリを割り当てます。



 constructor TVector.Create(ElementsCount: Word); begin FCount := ElementsCount; FData := nil; SetLength(FData, FCount); end; constructor TMatrix.Create(RowsCount, ColsCount: Word); begin FRowsCount := RowsCount; FColsCount := ColsCount; FData := nil; SetLength(FData, FRowsCount, FColsCount); end;
      
      





この段階でのクォータニオンはコンストラクターを必要としません。 データは静的配列に格納され、宣言の場所でメモリに展開されます。

ここで要素にアクセスするには、インデクサープロパティであるため、名前を省略してデフォルトにするのが便利です。 要求された要素へのアクセスは、有効な値のインデックスをチェックした後に発生します。 TVectorの実装を示します。



 function TVector.GetElement(Index: Word): Extended; begin {$R+} Result := FData[Pred(Index)]; end; procedure TVector.SetElement(Index: Word; Value: Extended); begin {$R+} FData[Pred(Index)] := Value; end;
      
      





この段階でオブジェクトを作成するには、次のコードを使用する必要があります。



 var V: TVector; . . . V := TVector.Create(3); V[1] := 1; V[2] := 2; V[3] := 3;
      
      





練習により、より簡潔な構文を使用してベクトルまたは行列を作成できるツールがあると便利です。 これを行うには、追加のコンストラクターを追加するとともに、暗黙的なキャスト操作を実装します。これにより、「:= "をオーバーロードできます。



 TQuaternion = record public . . . constructor Create(Q: TAbstractVector); class operator Implicit(V: TAbstractVector): TQuaternion; end; TVector = record public . . . constructor Create(V: TAbstractVector); overload; class operator Implicit(V: TAbstractVector): TVector; end; TMatrix = record public . . . constructor Create(M: TAbstractMatrix); overload; class operator Implicit(M: TAbstractMatrix): TMatrix; end;
      
      





そして実装:



 constructor TQuaternion.Create(Q: TAbstractVector); begin if Length(Q) <> 4 then raise EMathError.Create(WRONG_SIZE); Move(Q[0], FData[0], SizeOf(FData)); end; class operator TQuaternion.Implicit(V: TAbstractVector): TQuaternion; begin Result.Create(V); end; constructor TVector.Create(V: TAbstractVector); begin FCount := Length(V); FData := Copy(V); end; class operator TVector.Implicit(V: TAbstractVector): TVector; begin Result.Create(V); end; constructor TMatrix.Create(M: TAbstractMatrix); var I: Integer; begin FRowsCount := Length(M); FColsCount := Length(M[0]); FData := nil; SetLength(FData, FRowsCount, FColsCount); for I := 0 to Pred(FRowsCount) do FData[I] := Copy(M[I]); end; class operator TMatrix.Implicit(M: TAbstractMatrix): TMatrix; begin Result.Create(M); end;
      
      





ここで、ベクトルまたは行列を作成して初期化するには、次のように記述するだけで十分です。



 var V: TVector; M: TMatrix; . . . V := [4, 5, 6]; // M := [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
      
      





操作の過負荷



ここでは、たとえば、演算*のオーバーロードのみが実装され、マトリックスにベクトルが乗算されます。 その他の操作は、記事に添付されているファイルに記載されています 。 オーバーロードオプションの完全なリストはこちらです。



 TMatrix = record public . . . class operator Multiply(M: TMatrix; V: TVector): TVector; end; class operator TMatrix.Multiply(M: TMatrix; V: TVector): TVector; var I, J: Integer; begin if (M.FColsCount <> V.FCount) then raise EMathError.Create(WRONG_SIZE); Result.Create(M.FRowsCount); for I := 0 to M.FRowsCount - 1 do for J := 0 to M.FColsCount - 1 do Result.FData[I] := Result.FData[I] + M.FData[I, J] * V.FData[J]; end;
      
      





Multiply()メソッドの最初の引数は*記号の左側の行列、2番目の引数は*記号の右側の列ベクトルです。 作業の結果は、計算の過程でオブジェクトが作成される新しいベクトルです。 行列の列の数がベクトル要素の数と一致しない場合、例外が発生します。 プログラムでのこの操作の使用方法は次のとおりです。



 var V, VResult: TVector; M: TMatrix; . . . VResult := M * V;
      
      





ラッパー関数を使用して、配列のリテラルから「オンザフライ」で匿名のベクトルと行列を作成すると便利です。



 function TVec(V: TAbstractVector): TVector; begin Result.Create(V); end; function TMat(M: TAbstractMatrix): TMatrix; begin Result.Create(M); end; function TQuat(Q: TAbstractVector): TQuaternion; begin Result.Create(Q); end;
      
      





ラッパーの使用方法は次のとおりです。 前の例の式と同等のものを示します。



  V := TMat([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) * TVec([4, 5, 6]);
      
      





標準の操作に加えて、オブジェクトのタイプに特定のメソッド、たとえば転置や反転を追加すると便利です。 以下は、マトリックス反転(反転)メソッドの例です。 そのサイズにもかかわらず、これは私が見た中で最も高速です(高級言語で)。



 TMatrix = record public . . . function Inv: TMatrix; end; function TMatrix.Inv: TMatrix; var Ipiv, Indxr, Indxc: array of Integer; DimMat, I, J, K, L, N, ICol, IRow: Integer; Big, Dum, Pivinv: Extended; begin //  . if (FRowsCount <> FColsCount) then raise EMathError.Create(NOT_QUAD); Result := Self; DimMat := FRowsCount; SetLength(Ipiv, DimMat); SetLength(Indxr, DimMat); SetLength(Indxc, DimMat); IRow := 1; ICol := 1; for I := 1 to DimMat do begin Big := 0; for J := 1 to DimMat do if (Ipiv[J - 1] <> 1) then for K := 1 to DimMat do if (Ipiv[K - 1] = 0) then if (Abs(Result[J, K]) >= Big) then begin Big := Abs(Result[J, K]); IRow := J; ICol := K; end; Ipiv[ICol - 1] := Ipiv[ICol - 1] + 1; if (IRow <> ICol) then for L := 1 to DimMat do begin Dum := Result[IRow, L]; Result[IRow, L] := Result[ICol, L]; Result[ICol, L] := Dum; end; Indxr[I - 1] := IRow; Indxc[I - 1] := ICol; if Result[ICol, ICol] = 0 then raise EMathError.Create(SINGULAR); Pivinv := 1.0 / Result[ICol, ICol]; Result[ICol, ICol] := 1.0; for L := 1 to DimMat do Result[ICol, L] := Result[ICol, L] * Pivinv; for N := 1 to DimMat do if (N <> ICol) then begin Dum := Result[N, ICol]; Result[N, ICol] := 0.0; for L := 1 to DimMat do Result[N, L] := Result[N, L] - Result[ICol, L] * Dum; end; end; for L := DimMat downto 1 do if (Indxr[L - 1] <> Indxc[L - 1]) then for K := 1 to DimMat do begin Dum := Result[K, Indxr[L - 1]]; Result[K, Indxr[L - 1]] := Result[K, Indxc[L - 1]]; Result[K, Indxc[L - 1]] := Dum; end; end;
      
      





値でコピー



動的配列を使用してベクトルおよび行列の要素を保存すると、レシーバーオブジェクト( ":="の左側にあるもの)でそれらを完全にコピーしようとすると、この動的配列へのリンクのコピーが作成されます。

たとえば、式の評価後に行列Mの値を保存しようとすると、MStore行列も反転します。



 var M, MStore: TMatrix; . . . MStore := M; M := M.Inv;
      
      





値によるコピーを正しく実装するために、動的配列の最初の要素のアドレスからの負のオフセットに、長さの値とともに、この配列への参照カウンターが格納されるという事実を使用します。 カウンター値が0の場合、メモリマネージャーはこの配列を解放します。 カウンタ値が1の場合、これはメモリ内の配列インスタンスへの参照が1つのみであることを意味します。

したがって、コピーするときは、カウンターの値を分析し、1より大きい場合は、コピー先オブジェクトelement- by- elementにコピーすることにより、配列の完全なコピーを作成する必要があります。 以下は、動的配列入力パラメーターで渡された参照カウントの値が1を超える場合にのみTrueを返す関数のコードです。



 {$POINTERMATH ON} function NotUnique(var Arr): Boolean; begin Result := (PCardinal(Arr) - 2)^ > 1; end;
      
      





どの時点でフルコピーを実行する必要がありますか? この操作は非常に時間がかかるため、読み取りのためにベクトル\行列の要素にアクセスするときに実行する意味がありません。 オリジナルへの参照が少なくとも1,000あり、変更の対象でない場合、それらはすべて同じままです。 したがって、記録するアイテムにアクセスする場合にのみコピーする必要があります。 これを行うには、ベクトルと行列のSetElement()メソッドを変更し、最初にFData配列のインスタンスの一意性のチェックを追加します。



 procedure TVector.SetElement(Index: Word; Value: Extended); begin {$R+} CheckUnique; FData[Pred(Index)] := Value; end; procedure TVector.CheckUnique; begin if NotUnique(FData) then FData := Copy(FData); end; procedure TMatrix.SetElement(Row, Col: Word; Value: Extended); begin {$R+} CheckUnique; FData[Pred(Row), Pred(Col)] := Value; end; procedure TMatrix.CheckUnique; var I: Integer; begin if NotUnique(FData) then begin FData := Copy(FData); for I := 0 to Pred(FRowsCount) do FData[i] := Copy(FData[i]); end; end;
      
      





したがって、要素の値を変更しようとすると、リンクの一意性がチェックされ、確認されない場合、ビット単位のコピーが作成され、そこに変更が行われます。



注釈と自動初期化



ベクトルと行列の要素は、それらにメモリを割り当てた後にのみアクセスする必要があります。 要素の値は動的配列に格納され、そのサイズはオブジェクトのコンストラクターで設定されます。 暗黙的なコンストラクター呼び出しは、オブジェクトの初期化中、または式の評価の過程で発生する場合があります。



 var V: TVector; M: TMatrix; begin // V[1] := 1; // :    V := TVector.Create(4); //    M := TMatrix.Create(4, 4); //    // V := [1, 0, 0, 0]; //    // V := M * TVec([1, 0, 0, 0]); //    V[1] := 1; //    :  
      
      





暗黙のコンストラクタを使用すると、遅かれ早かれ、未作成のオブジェクトの要素へのアクセスが許可されたときにエラーが発生する可能性があります。 適切な形式の規則に従って、コンストラクターを明示的に呼び出す必要があります。

しかし、プログラムのベクトルと行列が数百と数千である場合はどうでしょうか? フィールドとして多くのベクトルと行列を使用するクラスの説明を検討してください。



 TMovement = record R: TVector; V: TVector; W: TVector; Color: TVector; end; TMovementScheme = class private FMovement: array[1..100] of TMovement; FOrientation: TMatrix; end;
      
      





TVector型とTMatrix型のすべてのフィールドを自動的に初期化する方法を開発する必要があります。必要な要素数とサイズに応じて、ベクトルと行列にメモリを割り当てます。 アノテーションメカニズム(またはDelphiの観点からは属性)は、これに役立ちます-型に任意のメタデータを追加できるツールです。 そのため、各ベクトルについて、その要素の数を事前に知る必要があり、行列については、行と列の数を知る必要があります。

属性クラスの作成規則に従って、ディメンションのデータをカプセル化するクラスを作成しましょう。



 TDim = class(TCustomAttribute) private FRowCount: Integer; FColCount: Integer; public constructor Create(ARowCount: Integer; AColCount: Integer = 0); overload; property RowCount: Integer read FRowCount; property ColCount: Integer read FColCount; end; constructor TDim.Create(ARowCount: Integer; AColCount: Integer = 0); begin FRowCount := ARowCount; FColCount := AColCount; end;
      
      





コンストラクターは行と列の数を取得します。ベクトルの場合、行の数のみで実行できます。 ここで、前のリストの型の定義に新しい注釈を追加します。



 TMovement = record [TDim(3)] R: TVector; [TDim(3)] V: TVector; [TDim(3)] W: TVector; [TDim(4)] Color: TVector; end; TMovementScheme = class private FMovement: array[1..100] of TMovement; [TDim(3, 3)] FOrientation: TMatrix; end;
      
      





以下は、属性から取得した情報に基づいて、TVector型とTMatrix型のオブジェクトを初期化するコードです。



 procedure Init(Obj, TypeInfoOfObj: Pointer; Offset: Integer = 0); const DefaultRowCount = 3; DefaultColCount = 3; VectorTypeName = 'TVector'; MatrixTypeName = 'TMatrix'; var RTTIContext: TRttiContext; Field : TRttiField; ArrFld: TRttiArrayType; I: Integer; Dim: TCustomAttribute; RowCount, ColCount: Integer; OffsetFromArray: Integer; begin for Field in RTTIContext.GetType(TypeInfoOfObj).GetFields do begin if Field.FieldType <> nil then begin RowCount := DefaultRowCount; ColCount := DefaultColCount; for Dim in Field.GetAttributes do begin RowCount := (Dim as TDim).RowCount; ColCount := (Dim as TDim).ColCount; end; if Field.FieldType.TypeKind = tkArray then begin ArrFld := TRttiArrayType(Field.FieldType); if ArrFld.ElementType.TypeKind = tkRecord then begin for I := 0 to ArrFld.TotalElementCount - 1 do begin OffsetFromArray := I * ArrFld.ElementType.TypeSize; if ArrFld.ElementType.Name = VectorTypeName then PVector(Integer(Obj) + Field.Offset + OffsetFromArray + Offset)^ := TVector.Create(RowCount) else if ArrFld.ElementType.Name = MatrixTypeName then PMatrix(Integer(Obj) + Field.Offset + OffsetFromArray + Offset)^ := TMatrix.Create(RowCount, ColCount) else Init(Obj, ArrFld.ElementType.Handle, Field.Offset + OffsetFromArray); end; end; end else if Field.FieldType.TypeKind = tkRecord then begin if Field.FieldType.Name = VectorTypeName then PVector(Integer(Obj) + Field.Offset + Offset)^ := TVector.Create(RowCount) else if Field.FieldType.Name = MatrixTypeName then PMatrix(Integer(Obj) + Field.Offset + Offset)^ := TMatrix.Create(RowCount, ColCount) else Init(Obj, Field.FieldType.Handle, Field.Offset) end; end; end; end;
      
      





Init()プロシージャは、コンテナオブジェクトのアドレスとそのRTTIデータを入力として受け取ります。 次に、コンテナのすべてのフィールドの再帰的なトラバースがあり、型名が「TVector」および「TMatrix」であるすべてのカウンタフィールドに対して、コンストラクタが明示的に呼び出されます。

Init()プロシージャを使用してTMovementSchemeクラスを改良します。



 TMovementScheme = class . . . public constructor Create; end; constructor TMovementScheme.Create; begin Init(Self, Self.ClassInfo); end;
      
      





任意のレコードに対してInit()を呼び出すバリアント:



 var Movement: TMovement; . . . Init(@Movement, TypeInfo(TMovement));
      
      





デフォルトでは、Init()は3つの要素と3x3行列を持つベクトルを作成するため、TMovementおよびTMovementScheme型の宣言では、属性[TDim(3)]および[TDim(3、3)]は省略でき、[TDim(4) ]。



このファイルは記事に添付されており、説明されているアイデアの実装が完全に記載されています。



All Articles