markrogoyski / math-php

Powerful modern math library for PHP: Features descriptive statistics and regressions; Continuous and discrete probability distributions; Linear algebra with matrices and vectors, Numerical analysis; special mathematical functions; Algebra
MIT License
2.33k stars 240 forks source link

Matrix class refactoring #307

Open Beakerboy opened 5 years ago

Beakerboy commented 5 years ago

I’m moving some functions around and want to get your input. My plan is to make a MatrixBase trait. This trait will house methods that can work on any matrix regardless of shape and data type. A good example is getDiagonalElements(). It just places the diagonals in a new array, and does not matter if the items are Complex objects or floats.

The idea would be that any new type of matrix can use this base trait instead of the current Matrix class. Matrix has all methods defined with normal math operators and these will fail on most other data types.

I am also creating a SquareMatrixBase trait which houses universal Square-only methods, like minorMatrix. This function just excludes a row and column, so will work on any data type. This way, any new type of matrix like ObjectMatrix, can use the MatrixBase, and a SquareObjectMatrix can extend ObjectMatrix and use the SquareMatrixBase trait to have access to those methods. I think I’ll take it one step further and make an ObjectSquareMatrixBase, this way a ComplexSquareMatrix can be made by extending a ComplexMatrix and using the SquareMatrix, and ObjectMatrix traits.

I’m thinking it might be good to have a MatrixInterface and SquareMatrixInterface. This way the universal methods can type hint that they work on any type of matrix.

markrogoyski commented 5 years ago

Thanks for thinking about how to improve the Matrix class. I've been thinking about some improvements I want to make as well. I've held off since you and I were working on things like QR and eigenvalues and didn't want to create any merge conflicts. Refactoring is always encouraged, and unit tests are there to make it safe to do so.

Also, there are some other changes I was thinking about and waiting until version 2.0 because there are some other breaking changes I want to make. But we can start now with refactoring without changing the API.

Regarding a MatrixBaseTrait kind of design, you will have to consider where class properties go. For example, most Matrix methods make use of $this->A since that holds the matrix data. It would be hard to reason about the Trait if it isn't the one that owns the A property. I think it would be a misuse of a Trait if it was using internal properties of classes it suspects it might be used by. There is a case where using PHPStorm is really helpful. It will flag your code immediately if your trait starts using properties it doesn't own.

The Matrix class is quite large and I agree it is long overdue for some refactoring. Making changes for the sake of making it smaller has the risk of making it more complex to reason about. Keep in mind what the intended purpose of each class/trait/interface is and keep to it. Someone looking at the code should obviously know that X is in the Y class and A is in the B trait or whatever. Good names help with this. Don't worry too much about getting it perfect. Design is iterative. And all programming involves design. I recommend doing it in small pieces over multiple pull requests. Also, let's coordinate a bit so we don't cause each other merge conflicts.

Interfaces are definitely the right choice for type declarations.

Beakerboy commented 5 years ago

As I think about it, I think the MatrixBase could be an abstract class instead of a trait.

Beakerboy commented 5 years ago

Please let me know your preference for which of the multiple inheritance patterns you prefer. Notice in this linked diagram that ComplexSquareMatrix and JacobianSquareMatrix are opposite each other.

And I just noticed a typo in my diagram. JacobianSquareMatrix would only have to use the JacobianBase trait, so that would make that version simpler.

Beakerboy commented 5 years ago

I also propose getting rid of the Vandermonde matrix. Instead we could have the MatrixFactory produce the appropriate NumericMatrix given an array and a integer for the polynomial degree. I’m thinking the Vandermonde matrix should be more like the identity matrix in that it is just a numerical matrix with a special form. The Factory can handle its construction without the need of a separate class structure.

Beakerboy commented 5 years ago

This might be a better diagram. You should be able to clone and edit your own version: https://sketchboard.me/lBx6LOlXiNoe#/

markrogoyski commented 5 years ago

I made my own version.

FYI, instead of using PowerPoint or sketch tools or whatever, I recommend using PlantUML for these kinds of things.

Go to this Web page: https://www.planttext.com/

And copy and paste in this PlantUML diagram:

@startuml

title MathPHP - Matrix Class Diagram

interface Matrix
abstract class MatrixBase implements Matrix

class NumericMatrix extends MatrixBase
class ObjectMatrix extends MatrixBase

class NumericSquareMatrix extends NumericMatrix

class ComplexMatrix extends ObjectMatrix
class ComplexSquareMatrix extends ComplexMatrix

class ObjectSquareMatrix extends ObjectMatrix

class FunctionMatrix extends MatrixBase
class FunctionSquareMatrix extends FunctionMatrix

class JacobianMatrix extends FunctionMatrix
class JacobianSquareMatrix extends JacobianMatrix

class SquareMatrixTrait
NumericSquareMatrix *-- SquareMatrixTrait
ComplexSquareMatrix *-- SquareMatrixTrait
ObjectSquareMatrix *-- SquareMatrixTrait
FunctionSquareMatrix *-- SquareMatrixTrait
JacobianSquareMatrix *-- SquareMatrixTrait

@enduml

PlantUML syntax is pretty easy to learn. If you have to make changes, it is just changing some text. You can check it into git since it is just text. PHPStorm has a PlantUML plugin to do it in the IDE.

PlantUML documentation: http://plantuml.com/en/

Beakerboy commented 5 years ago

Nice! Try this.

I just had the JacobianMatrix as an example of what might possibly extend the FunctionMatrix. I think this is what we would need to be set up for a successful v1.0.

markrogoyski commented 5 years ago

I have a question about how I should be reasoning about what the relationships are with the complex matrices.

For example, a ComplexSquareMatrix is not a ComplexMatrix. Instead, they have a ComplexMatrixTrait. Since these two matrices are not related, you could not have a type declaration of a generic ComplexMatrix, because Complex and ComplexSquare do not share a common type.

What is the reason for the ComplexTrait, and for not having a direct hierarchy of Object Matrix <- ComplexMatrix <- ComplexSquareMatrix?

Beakerboy commented 5 years ago

Good point on the Complex Hierarchy. Is this better?

Edit: also just added the ColumnVector and RowVector. Although I feel they are like the Vandermonde and add little to the library. There's nothing to them that could not be performed by the MatrixFactory and the existing Matrix class

markrogoyski commented 5 years ago

I don't think traits can extend traits. Traits are just a means to copy and paste code into a class in scenarios where multiple inheritance might have been used but isn't a thing in PHP.

I agree with things like Column and Row Vectors. The MatrixFactory can have multiple create methods to handle this and just produce a regular matrix.

Beakerboy commented 5 years ago

A trait can use another trait and add it's own method, so it's almost like inheritance. updated Diagram

markrogoyski commented 5 years ago

Cool. Classes and traits seem to make sense.

I'm not sure I understand why every matrix would implement ObjectArithmetic. I thought an ObjectMatrix (and only an ObjectMatrix) would contain values that themselves where objects that implemented ObjectArithmetic.

Beakerboy commented 5 years ago

ObjectArithmetic currently defines an object that has define add(), subtract(), and multiply() methods. Since we want all of our matrices to implement those functions, it makes sense to extend it. I mentioned this elsewhere, but if a matrix extends Object Arithmetic, then we can use a matrix as the object type within another matrix, in essence creating a block matrix.

markrogoyski commented 5 years ago

Oh nice. An InceptionMatrix! OK. I get it. Thanks.

Looks good. I'll think it over some more and see if I can come up with any issues with this design.

markrogoyski commented 5 years ago

Another matrix I am thinking of adding is a ZeroOneMatrix. It would probably inherit from the NumericMatrix. A zero-one matrix is a discrete math/computer science-y thing for doing boolean operations and relations.

markrogoyski commented 5 years ago

FYI, in the develop branch I've removed the Vandermonde matrices and added a specific MatrixFactory method to create it as a regular matrix. I've also added a factory method for diagonal matrices.

Beakerboy commented 5 years ago

I would say a zero one matrix would be a new matrix type that extends the MatrixBase, and could be called BooleanMatrix. The matrix would instead contain bool data types. The matrix would need to implement add(), subtract(), and multiply(). I would say that add() is the join of two zero-one matrixes. Would subtract() be undefined? The Boolean product would be multiply().

markrogoyski commented 5 years ago

Having a matrix of boolean values is an interesting idea. The reason I was thinking to use a NumericMatrix of integer values 1 and 0 was that since matrices are immutable, and all operations on a matrix produce a new matrix, you would be able to do all the other operations that are defined on a matrix, as 1 and 0 are values that would work with every other already-defined operation on a matrix. Whereas if you have boolean types, most of the existing methods don't work. The constructor could ensure that only integer 1s and 0s got passed in. It could implement this new BooleanInterface or something that had the new methods that only work on these new matrices.

Just something I was considering and no rush to implement a ZeroOne or Boolean Matrix or whatever it ends up being called.

Beakerboy commented 5 years ago

What special methods would a zero-one matrix have besides meet(), join(), and multiply()?

markrogoyski commented 5 years ago

My old Discrete Math textbook defines these operations on a Zero-One Matrix:

Beakerboy commented 5 years ago

Updated Diagram

markrogoyski commented 5 years ago

Thanks for continuing to iterate on this, and apologies for the delay in really looking at this.

Would it make more sense, and make for a simpler relationship if MatrixBase itself implemented the Matrix interface?

For example:

@startuml
title MathPHP - Matrix Class Diagram

interface ObjectArithmetic{
    add()
    subtract()
    multiply()
}
interface Matrix {
    getObjectType(): string
}
interface SquareMatrix extends Matrix{
    det()
    trace()
}
abstract class MatrixBase implements Matrix, ObjectArithmetic{
  int $m
  int $n
  array $A
  isSquare(): boolean
  transpose(): Matrix
  directSum(): Matrix
  augment(): Matrix
  submatrix(): Matrix
}

class NumericMatrix extends MatrixBase{
    add(): NumericMatrix
    subtract(): NumericMatrix
    multiply(): NumericZatrix
    getObjectType(): string
}
class ZeroOneMatrix extends MatrixBase{
    add(): ZeroOneMatrix
    subtract(): ZeroOneMatrix
    multiply(): ZeroOneMatrix
    join(): ZeroOneMatrix
    meet(): ZeroOneMatrix
    booleanProduct(): ZeroOneMatrix
    booleanPower(): ZeroOneMatrix
    getObjectType(): string
}
class ObjectMatrix extends MatrixBase{
    add(): ObjectMatrix
    subtract():ObjectMatrix
    multiply(): Objectmatrix
    getObjectType(): string
}

class NumericSquareMatrix extends NumericMatrix implements SquareMatrix{
    det()
    trace()
}

class ComplexMatrix extends ObjectMatrix{
    conjugateTranspose()
}
class ComplexSquareMatrix extends ComplexMatrix implements SquareMatrix

class ObjectSquareMatrix extends ObjectMatrix implements SquareMatrix{

}

class FunctionMatrix extends MatrixBase {
    add(): FunctionMatrix
    subtract(): FunctionMatrix
    multiply(): Functionmatrix
    getObjectType(): string
}
class FunctionSquareMatrix extends FunctionMatrix implements SquareMatrix{
    det()
    trace()
}

class SquareMatrixTrait<< (T,orchid) >>{
    SquareMatrix $A⁻¹
    isSquare(): boolean
    leadingPrincipalMinor(): Squarematrix
    minorMatrix(): SquareMatrix
}
NumericSquareMatrix *-- SquareMatrixTrait
FunctionSquareMatrix *-- SquareMatrixTrait

class ObjectSquareMatrixTrait<< (T,orchid) >>{
        det()
    trace()
}
ObjectSquareMatrixTrait *-- SquareMatrixTrait
ObjectSquareMatrix *-- ObjectSquareMatrixTrait
ComplexSquareMatrix *-- ObjectSquareMatrixTrait
@enduml
Beakerboy commented 5 years ago

MatrixBase can’t actually “implement” ObjectArithmetic, but it certainly could declare the functions as abstract, and force the children to provide the details.

markrogoyski commented 5 years ago

Yeah, it could implement the interface by making the methods abstract with no implementation. It would be able to, in the PHP sense, "implement" the interface even though it didn't provide its own implementation.

For example:

Interactive shell

php > interface Test { public function test(); }
php > abstract class TestA implements Test { abstract public function test(); }
php > class TestImplA extends TestA { public function test() {} }
php > print_r(class_implements(TestImplA::class));
Array
(
    [Test] => Test
)
php
Beakerboy commented 5 years ago

The reason I had not set it up that way is because I hadn’t thought about abstract methods fulfilling the implementation obligation. That’s a much better design.

Beakerboy commented 3 years ago

Updated to include the DiagonalMatrix class

markrogoyski commented 3 years ago

Hi @Beakerboy,

I started to implement some of this in my v2.0 migration branch by renaming Matrix to NumericMatrix and MatrixBase to Matrix. As I was looking at other classes in the class hierarchy, I was wondering what to do about the FunctionMatrix classes. They aren't really sibling Matrices with the other Matrix classes, as most of the computation methods will error out because there are no numeric values in them. They seem to be more like NumericMatrix Factories with their evaluate method.

Would it make more sense to take these out of the Matrix hierarchy and just make them utility helper factory classes for producing NumericMatrices? If I did that, I don't think you need the FunctionSquareMatrix class anymore, as the new FunctionMatrixFactory or whatever it is would just produce the appropriate NumericMatrix or NumericSquareMatrix based on how it was constructed with functions when evaluated with numeric parameters.

Thoughts?

markrogoyski commented 3 years ago

Current matrix object graph looks like this in the latest v2.0.0 release.

@startuml
title MathPHP - Matrix Class Diagram

abstract class Matrix {

}

class NumericMatrix extends Matrix {

}

class NumericSquareMatrix extends NumericMatrix {

}

class NumericDiagonalMatrix extends NumericSquareMatrix {

}

class RowEchelonForm extends NumericMatrix {

}

class ReducedRowEchelonForm extends RowEchelonForm {

}

class RowVector extends NumericMatrix {

}

class ColumnVector extends NumericMatrix {

}

class ObjectMatrix extends Matrix {

}

class ObjectSquareMatrix extends ObjectMatrix {

}

interface ObjectArithmetic {

}

ObjectMatrix *-- ObjectArithmetic

@enduml