A implementação de algoritmos inerciais no exemplo da modelagem lógica de circuitos digitais

1. Introdução


Prosseguimos para a segunda parte do tópico dedicada aos autômatos aninhados. No primeiro, examinamos algoritmos recursivos que, tendo um modelo de autômatos aninhados e conectando recursos de OOP, não eram tão difíceis de implementar. Mas as possibilidades de autômatos aninhados não se limitam a isso. Assim, ao descrever o modelo de controle dos programas de autômatos, foram determinados algoritmos inerciais, que também se baseiam na idéia de incorporar autômatos. Os algoritmos inerciais são difíceis de imaginar na estrutura do diagrama de blocos usual do modelo computacional, que não fornece o retorno do controle ao ponto anterior à chamada da sub-rotina. Mas devo dizer que os autômatos convencionais também fornecem a abolição das transições "on the fly". No entanto, para os autômatos, isso não só pode ser imaginado, mas também implementado.

Algoritmos inerciais e modelagem lógica de tópicos relacionados a circuitos digitais. E não tanto porque o atraso inercial deu o nome à classe de algoritmos em consideração, mas quanto o significado das ações atrasadas do atraso inercial, que foi transferido para o significado de seu trabalho. Embora, claro, não seja o nome. Na UML, algoritmos semelhantes são implementados usando o conceito de estado histórico. A analogia é direta, embora a modelagem de circuitos digitais em UML, em regra, esteja fora de questão. Só que as situações ao retornar ao ponto de partida, se algo o leva a fazê-lo, parecem ser as mais naturais. Exemplos incluem recusa de compra de bilhetes, cancelamento de operações / transações bancárias, desconexão de uma conexão de rede, etc. etc. No final, conforme declarado na UML,sem o uso de estados históricos, a implementação de tais algoritmos não seria tão "bonita" [1].

O tópico da modelagem lógica de circuitos digitais é extenso e interessante por si só. E uma nova leitura não a machuca. Como veremos, a tecnologia de programação automática pode oferecer uma maneira ainda melhor de descrever, implementar e modelar logicamente circuitos digitais. Saberemos isso da mesma forma, buscando o objetivo principal - considerar uma classe interessante, útil e, finalmente, apenas uma bela classe de algoritmos que é natural para os autômatos, mas bastante difícil de implementar em outros modelos computacionais.

2. Modelagem lógica de circuitos digitais


A literatura geralmente considera dois métodos de modelagem de circuitos digitais - compilação e evento. A compilação é limitada em suas capacidades, porque não leva em consideração atrasos de elementos, requer elementos de classificação e quebra de feedback [2]. O método de evento não possui essas restrições e baseia-se no rastreamento de eventos associados a alterações nos valores do sinal dentro do circuito. Não consideraremos a compilação, mas focaremos na comparação do evento com o método implementado no âmbito das possibilidades de programação automática, que chamaremos mais adiante, de maneira automática.

Como exemplo, pegue o circuito de [2], mostrado na Fig. 1. Diagramas de seu trabalho a partir da fonte são mostrados na Fig. 2. Duas opções são consideradas: com um único atraso, quando os elementos lógicos do circuito tiverem o mesmo atraso, e com um atraso distribuído, quando os elementos B e E tiverem um atraso duas vezes maior que o restante dos elementos do circuito.
imagem

FIG. 1. Um exemplo de um circuito.

imagem

FIG. 2. Exemplos de modelagem: a - um único atraso; b - atraso distribuído.

Com o método de simulação automática, você nem precisa inventar algo, porque não há necessidade de criar uma linguagem suficientemente específica para descrever um circuito e estruturas que implementam relacionamentos de circuito, e não precisa de algoritmos suficientemente específicos para detectar eventos no processo de modelagem de um circuito, que servem como base para a construção de diagramas do circuito (para uma descrição de ambos, consulte [2] )

No caso de autômatos, são criados os modelos de elementos lógicos comuns à tecnologia de autômatos para o design do programa, incluídos na biblioteca de elementos lógicos (BLE). Além disso, com base nesta biblioteca, é criado um conjunto de processos de autômatos paralelos correspondentes ao número de elementos do circuito, entre os quais as conexões são indicadas usando os canais de entrada / saída dos modelos de elementos lógicos (para isso, no ambiente VKPA, as variáveis ​​locais do processo geralmente são suficientes). Em conclusão, o modelo de circuito é complementado por processos-geradores de sinais de entrada e processos de exibição de diagramas de sinais.

Os resultados da simulação do exemplo considerado no ambiente VKPa, mostrados na Fig. 3 coincidem completamente com os diagramas na Fig. 2. É verdade que não foi imediatamente possível alcançar tal coincidência. E não por problemas com os modelos, mas porque, de fato, o “método de seleção científica” teve que calcular a duração dos sinais de entrada, o que, como se viu, tem um impacto significativo. Mas em [2] nenhuma palavra foi dita sobre isso, nem sobre os parâmetros dos sinais de entrada. Foi alcançado um acordo completo ao descobrir que 1) a duração igual a três vezes o atraso é necessária e 2) o desvio do sinal (b) em relação ao sinal (a) deve ser igual ao atraso da unidade. Para explicar este problema, consulte a fig. A Figura 4 mostra os diagramas de sinais para diferentes durações dos sinais de entrada (e isso sem levar em consideração o deslocamento).

imagem

FIG. 3. A simulação resulta em VKPa: a - um único atraso; b - atraso distribuído.

imagem

FIG. 4. Resultados da simulação com diferentes durações do sinal de entrada

Considere outro exemplo de circuito da mesma fonte [2]. Seu esquema e diagramas de tempo de trabalho são mostrados na fig. 5. Na estrutura do método de evento, para “calcular” o diagrama de tempo, foram necessárias 20 etapas de simulação (para mais detalhes, consulte [2]). Mas, como afirmado lá, um algoritmo ainda mais complexo e, consequentemente, um número ainda maior de etapas são necessários se o tipo de atraso inercial for escolhido. No nosso caso (o caso do método de modelagem automática), tendo o modelo anterior, precisamos de “20 cliques” com o mouse para ir para o diagrama na Fig. 5, removendo elementos desnecessários do circuito original. Os resultados da simulação de circuito obtidos em VKPa são mostrados na Fig. 6

imagem

FIG. 5. Um exemplo de circuito e seu gráfico de tempo.

Além disso, o diagrama na Fig. 5 adicionamos paralelo ao elemento OR elemento I. Gráfico d na fig. 6 exibe sua operação no caso de um único atraso. Se definirmos um grande atraso e definir o tipo de atraso inercial, o gráfico d se transformará em uma linha reta. Portanto, o elemento E com um atraso inercial maior que um valor único não perderá o pulso gerado em suas entradas por uma combinação desses sinais de entrada a e b. Observe que manipular o tipo de atraso faz sentido apenas para elementos com um atraso maior que um.

imagem

FIG. 6. Modelando o circuito na Fig. 5 e o elemento E (d).

3. Implementação de elementos lógicos


No caso geral, qualquer elemento lógico pode ser representado como uma conexão em série de dois blocos (veja a Fig. 7) - um elemento lógico ideal sem atraso e um bloco, que pode ser considerado como atraso de propagação (atraso de transporte) ou atraso inercial [2].

imagem

FIG. 7. Modelo de elemento lógico atrasado.

Um elemento lógico ideal é muito facilmente implementado por uma função lógica normal, e o modelo de bloco de atraso pode ser representado na forma de um modelo de autômato - um modelo universal que inclui transporte e atraso inercial. Um modelo desse atraso universal é mostrado na Fig. 8 e modelos de autômatos aninhados para ele são mostrados na Fig. 9. Sua implementação em C ++ e como parte da tecnologia de programação automatizada é mostrada na Listagem 1.

imagem

FIG. 8. O modelo do atraso universal.

imagem

FIG. 9. Modelos de autômatos aninhados para atraso universal.

As máquinas na Fig. 8 e fig. 9 são rifles de assalto Mili-Moore mistos. A operação do autômato principal na Fig. 8. começa criando variáveis ​​locais e inicializando as referências na ação y12 (o predicado x12 verifica tudo isso). O estado intermediário "ss" é necessário para que o predicado x3 funcione corretamente, que possui um link para uma variável de entrada, que pode não ser inicializada. Do estado "ss", o modelo entra no estado correspondente à saída de atraso, causando um autômato aninhado. Observe que as ações sob os estados de um autômato (ações do autômato de Moore) serão iniciadas somente após a conclusão da operação de um autômato aninhado. Eles definirão o valor do atraso atual e o estado da variável de saída.

A ação y13, se um valor de atraso for definido, cria o autômato aninhado necessário, dependendo do tipo de atraso. O autômato de atraso de transporte incorporado simplesmente conta o valor definido dos ciclos discretos do relógio de ponto (a duração do atraso é determinada pelo número de ciclos discretos) e o atraso inercial controla adicionalmente o nível do sinal de entrada. Nesse caso, observamos que o valor retornado do predicado x3 depende do estado atual do autômato de nível superior.

A implementação de autômatos na Fig. 8, 9 reflete a Listagem 3. Considerando o código, você deve prestar atenção ao método virtual f (), que, por um lado, implementa uma ou outra função lógica abstrata sobreposta e, por outro lado, executa, se especificado, inversão. Tudo isso é necessário para a implementação de modelos derivados de elementos lógicos. A Listagem 2 demonstra a implementação de um tal portão NAND.

Listagem 1. Implementando um elemento lógico de atraso universal
#include "lfsaappl.h"

extern LArc TBL_DiscreteTime[];
class FDiscreteTime :											
	public LFsaAppl											
{													
public:													
    enum {cvarINE, cvarExlusiveOR, cvarOrNot};
    void MooreAction();
	bool	FCreationOfLinksForVariables();								
    LFsaAppl* Create(CVarFSA *pCVF) { Q_UNUSED(pCVF) return new FDiscreteTime(nameFsa); }
	bool FInit();											
    FDiscreteTime(string strNam, LArc* pTBL = TBL_DiscreteTime);
	~FDiscreteTime(void);										
	CVar *pVarType;		// delay type 0-transport; 1- inertial
	CVar *pVarX1;		// input variable
	CVar *pVarStrNameX1;	// input variable name
    	CVar *pVarIfNotX1;	// inverse of the first input variable
    	CVar *pVarY1;		// output variable
	CVar *pVarStrNameY1;	// output variable name
	CVar *pVarValue01;	// delay value from 0 to 1
	CVar *pVarValue10;	// delay value from 1 to 0
    	CVar *pVarNegationY;// output inversion 0 - no inversion; 1- inversion
    	virtual int x3();	// input analysis
	virtual int x12();	// link setup analysis
    	virtual bool f();
    	int nTypeElement;
protected:
// predicates												
    int x1();
// actions												
	void y1(); void y4(); void y5(); void y6(); void y7(); void y12(); void y13(); 
    bool bType{false};	// delay type: false - transport; true - inertial;
    bool bX1{false};
    int nCurrent{0};
    int nDelay{0};		// tech. delay counter value
    LFsaAppl	*pFCall{nullptr};
    friend class FCallTransport;
    friend class FCallInertial;
};		

class FCallTransport :
	public LFsaAppl
{
public:
	void MooreAction();
	FCallTransport(FDiscreteTime	*pFDiscreteTime);
	FDiscreteTime	*pFDiscreteTime;
protected:
	int x1();
};

class FCallInertial :
	public LFsaAppl
{
public:
	void MooreAction();
	FCallInertial(FDiscreteTime	*pFDiscreteTime);
	FDiscreteTime	*pFDiscreteTime;
protected:
    int x1(); int x3();
};

#include "stdafx.h"
#include "FDiscreteTime.h"											
#include "VARFSA/SetVarFsaLibrary.h"
//=====================================================================
//		Delay model at the upper structural level of the view
//=====================================================================
LArc TBL_DiscreteTime[] = {
    LArc("st",	"st","^x12","y12"),	//
    LArc("st",	"ss","x12", "--"),	//
    LArc("ss",	"s1","x3",  "y7y6y13"),// transition to a single state
    LArc("ss",	"s0","^x3", "y4y5y13"),// transition to zero state
// creation of a nested automaton at the transition to a single state
    LArc("s0",	"s1","x3",  "y13"),    
// creation of a nested automaton at the transition to the zero state
    LArc("s1",	"s0","^x3", "y13"),    
    LArc()
};
FDiscreteTime::FDiscreteTime(string strNam, LArc* pTBL):
    LFsaAppl(pTBL, strNam, nullptr, nullptr)
{ }
													
FDiscreteTime::~FDiscreteTime(void) { if (pFCall) delete pFCall; }

bool FDiscreteTime::FInit() {										
    FCreationOfLinksForVariables();
    return true;
}

bool FDiscreteTime::FCreationOfLinksForVariables()
{
// Local variables
    pVarNegationY = CreateLocVar("negation", CLocVar::vtBool, "output inversion: 0-without inversion / 1-inversion");
    pVarType = CreateLocVar("type", CLocVar::vtBool, "delay type: 0-transp / 1-inertia");
    pVarY1 = CreateLocVar("y", CLocVar::vtBool, "local output");
    pVarX1 = CreateLocVar("x1", CLocVar::vtBool, "local input");
    pVarValue01 = CreateLocVar("value to 1", CLocVar::vtInteger, "delay value from 0 to 1");
    pVarValue10 = CreateLocVar("value to 0", CLocVar::vtInteger, "delay value from 1 to 0");
    pVarStrNameX1 = CreateLocVar("strNameX1", CLocVar::vtString, "name of external input variable (x)");
    pVarStrNameY1 = CreateLocVar("strNameY", CLocVar::vtString, "name of external output variable (y)");
    pVarIfNotX1 = CreateLocVar("not(x1)", CLocVar::vtBool, "1st input inversion: 0-without inversion / 1-inversion");
    string str;
    str = pVarStrNameX1->strGetDataSrc();
    if (str != "") { pVarX1 = pTAppCore->GetAddressVar(pVarStrNameX1->strGetDataSrc().c_str(), this);	}
    str = pVarStrNameY1->strGetDataSrc();
    if (str != "") { pVarY1 = pTAppCore->GetAddressVar(pVarStrNameY1->strGetDataSrc().c_str(), this);	}
    return true;
}
// predicates
int FDiscreteTime::x1() { return nCurrent == nDelay; }
//  
int FDiscreteTime::x3() {
    if (bool(pVarNegationY->GetDataSrc())) return !f();
    return f();
}
//
int FDiscreteTime::x12() { return pVarX1 != nullptr; }
//
bool FDiscreteTime::f() {
    bX1 = bool(pVarX1->GetDataSrc());
    if (bool(pVarIfNotX1->GetDataSrc())) bX1 = !bX1;
    return bX1;
}
// actions
// +1 to the current delay value
void FDiscreteTime::y1() { nCurrent++; }
// setting the delay value when switching from 0 to 1
void FDiscreteTime::y4() { nDelay = int(pVarValue01->GetDataSrc()); }
// setting output to zero
void FDiscreteTime::y5() { pVarY1->SetDataSrc(nullptr, 0.0); }
// setting output to unit
void FDiscreteTime::y6() { pVarY1->SetDataSrc(nullptr, 1); }
// setting the delay value when switching from 1 to 0
void FDiscreteTime::y7() { nDelay = int(pVarValue10->GetDataSrc()); }
//
void FDiscreteTime::y12() { FInit(); }
// creation, if a delay is determined, of the necessary nested automaton
void FDiscreteTime::y13() {
	nCurrent = 0;
	if (pFCall) { delete pFCall; pFCall = nullptr; }
	if (x1()) return;
	bType = pVarType->GetDataSrc();		// set delay type
	if (bType) pFCall = new FCallInertial(this);
	else pFCall = new FCallTransport(this);
	if (pFCall) pFCall->FCall(this);
}

void FDiscreteTime::MooreAction()
{
	string strState = FGetState();
	if (strState=="s0")	{ 
        y4(); y5();		// y4) setting the delay value when switching from 0 to 1; y5) set the output to zero
    }
	else if (strState=="s1") { 
        y7(); y6();		// y7) setting the delay value when switching from 1 to 0; y6) setting the output to one
    }
}
//=====================================================================
//				Transport delay
//=====================================================================
static LArc TBL_CallTransport[] = {
	LArc("s5","s5","^x1",	"--"),		//
	LArc("s5","00","x1",	"--"),		// 
	LArc()
};

FCallTransport::FCallTransport(FDiscreteTime	*pFI):
    LFsaAppl(TBL_CallTransport, "FCallTransport", nullptr, nullptr)
{
	pFDiscreteTime = pFI;
}
// . == 
int FCallTransport::x1() { return pFDiscreteTime->x1(); }
//
void FCallTransport::MooreAction()
{
	string strState = FGetState();
	if (strState=="s5")	{ pFDiscreteTime->y1(); }
}
//=====================================================================
//				Inertial delay
//=====================================================================
static LArc TBL_CallInertial[] = {
	LArc("s5",	"s5","^x1x3",	"--"),		//
	LArc("s5",	"00","x1x3",	"--"),		//
	LArc("s5",	"XX","^x3",	"--"),		//
	LArc()
};

FCallInertial::FCallInertial(FDiscreteTime	*pFI):
    LFsaAppl(TBL_CallInertial, "FCallInertial", nullptr, nullptr)
{
	pFDiscreteTime = pFI;
}
// . == 
int FCallInertial::x1() { return pFDiscreteTime->x1(); }
// input value (depends on the name of the current state of the main automaton)
int FCallInertial::x3() {
	string strState = FGetStateUp();
	bool bX = pFDiscreteTime->x3();
    if (strState == "s0") return bX;
    if (strState == "s1") return !bX;
	if (strState == "st") {
		string str = pFDiscreteTime->FGetNextState();
		bX = pFDiscreteTime->x3();
		if (!bX) {
			if (x1()) {
                if (str == "s0") return !bX;
                if (str == "s1") return bX;
			}
            else return true;
		}
		return true;
	}
	else return bX; 
}
//
void FCallInertial::MooreAction()
{
	string strState = FGetState();
	if (strState=="s5")	{ pFDiscreteTime->y1(); }
}


Listagem 2. Implementando uma porta NAND
#include "lfsaappl.h"

#include "../FDiscreteTime.h"
extern LArc TBL_DiscreteTime[];
class FIne :
	public FDiscreteTime
{
public:
    bool	FCreationOfLinksForVariables() override;
    LFsaAppl* Create(CVarFSA *pCVF) override { Q_UNUSED(pCVF) return new FIne(nameFsa); }
    bool FInit() override;
    FIne(string strNam, LArc* TBL = TBL_DiscreteTime);
    ~FIne(void);
	CVar *pVarX2;		//  
	CVar *pVarStrNameX2;	//    
	CVar *pVarIfNotX2;	//    
	virtual bool f() override;
protected:
    	int x12() override;
    	bool bX2;
};

#include "stdafx.h"
#include <Ine/FIne.h>
#include "VARFSA/SetVarFsaLibrary.h"

FIne::FIne(string strNam, LArc* pTBL):
    FDiscreteTime(strNam, pTBL)
{
    nTypeElement = FDiscreteTime::cvarINE;
}

FIne::~FIne(void) { }

bool FIne::FInit() {										
// 	 		
	FCreationOfLinksForVariables();
//	 										
	return true;										
}

bool FIne::FCreationOfLinksForVariables() {
//  
	FDiscreteTime::FCreationOfLinksForVariables();
	//      x1, x2
    pVarIfNotX2 = CreateLocVar("not(x2)", CLocVar::vtBool, " 2- : 0- /1-");
	pVarX2 = CreateLocVar("x2", CLocVar::vtBool, " 2- ");
    //  ,         x2
	pVarStrNameX2 = CreateLocVar("strNameX2", CLocVar::vtString, "  2- .(x)");
	// :   ,     
    string str = pVarStrNameX2->strGetDataSrc();
    if (str != "") { pVarX2 = pTAppCore->GetAddressVar(pVarStrNameX2->strGetDataSrc().c_str(), this); }
//
	return true;
}
//
int FIne::x12() { return FDiscreteTime::x12() && pVarX2 != nullptr; }
//
bool FIne::f() {
    //  1- 
    FDiscreteTime::f();
    //   
    bX2 = bool(pVarX2->GetDataSrc());
    if (bool(pVarIfNotX2->GetDataSrc())) bX2 = !bX2;
    //   : 
    return bX1&&bX2;
}


Graças ao OOP, o código do modelo do elemento lógico AND NÃO é notavelmente menor que o código do atraso "pai". Além disso, é amplamente determinado pela necessidade de criar uma entrada adicional para o modelo do elemento AND. Se, no nível estrutural, os modelos corresponderem ao número de entradas / saídas, o código será ainda menor. Portanto, o código de implementação para a porta XOR gerada a partir de um modelo estruturalmente semelhante ao elemento AND-NOT é mostrado na Listagem 3.

Listagem 3. Implementação exclusiva de gateway OR
#include <Ine/FIne.h>

extern LArc TBL_DiscreteTime[];
class FExlusiveOR :
    public FIne
{
public:
    LFsaAppl* Create(CVarFSA *pCVF) { return new FExlusiveOR(nameFsa); }
    FExlusiveOR(string strNam, LArc* TBL = TBL_DiscreteTime);
    ~FExlusiveOR(void);
protected:												
    bool f();
};

#include "stdafx.h"
#include "FExlusiveOR.h"

FExlusiveOR::FExlusiveOR(string strNam, LArc* pTBL):
    FIne(strNam, pTBL)
{
    nTypeElement = FDiscreteTime::cvarExlusiveOR;
}

FExlusiveOR::~FExlusiveOR(void) { }
//
bool FExlusiveOR::f() {
    FIne::f();
    return (bX1&&bX2)||(!bX1&&!bX2);
}


Aqui, o método f () da classe FExlusiveOR chama o método f () da classe FIne apenas para ler os valores das variáveis ​​de entrada, sobrepondo o valor da função NAND ao valor calculado de acordo com a função lógica da nova classe. Na imagem e semelhança, modelos de qualquer outro elemento lógico de duas entradas podem ser criados.

3. Conclusões


Para implementar qualquer circuito digital, o chamado conjunto funcionalmente completo de elementos lógicos é suficiente. Pode ser, por exemplo, um conjunto de elementos AND, OR, NOT. Examinamos a implementação dos seguintes elementos lógicos - atrasos, que podem ser um inversor, elementos AND-NOT e EXCLUSIVE OR. Eles se relacionam com os elementos básicos que fazem parte de um conjunto funcionalmente completo. Para implementar e / ou simular qualquer esquema, resta adicionar à biblioteca, por exemplo, um elemento OR-NOT ou implementar um elemento personalizado universal configurado para um elemento de um conjunto funcionalmente completo. E isso já será suficiente.

Como resultado, com base nos modelos acima, temos na pessoa do ambiente VKPa um ambiente completo para a modelagem lógica de circuitos digitais, que permite gerar qualquer número de processos a partir de qualquer elemento da biblioteca, configurá-los e estabelecer conexões entre eles. Ao mesmo tempo, a simulação será mais detalhada (levando em consideração todos os tipos de atrasos), mais flexível (você pode configurar os elementos individualmente) do que no caso dos dois métodos típicos de modelagem lógica mencionados acima - compilação e evento. E não será um ambiente especializado, como o modelo de processos de software permanece inalterado e permanecerá tão universal quanto qualquer outro ambiente de programação orientado a objetos baseado no uso da linguagem C ++.

E mais sobre ... dor. Era uma vez, confrontado com o que há de errado na literatura científica ou, mais precisamente, uma descrição de um elemento lógico essencial como um gatilho RS é realmente analfabeta, bem como o fato de que mesmo engenheiros legais de eletrônica não sabem como funciona em detalhes gatilho, gastei muito tempo para descobrir isso. E posso relatar que, no momento do problema do gatilho, pelo menos para mim não há espaços em branco (veja detalhes [3]). E, portanto, resta apenas se surpreender que, em geral, nada esteja mudando. Como a descrição tabular do gatilho foi dada anteriormente, ainda é hoje, pois havia os notórios estados proibidos, e eles permaneceram os mesmos que se queixaram da mudança imprevisível do gatilho ao sair do estado proibido, por isso continuam na mesma ignorância. E isso apesarque exemplos de uma descrição suficientemente precisa de seu comportamento já são conhecidos (veja, por exemplo, [4]).

Verdadeiramente, "Tuas obras são maravilhosas, Senhor!" Parece que não, na cabeça desses “descritores de acionamento RS” ... programação automática e o método considerado de modelagem de circuitos lógicos no ambiente VKPa. E se você retornar ao tópico do artigo, por exemplo, usando o modelo acima do elemento NAND, poderá criar facilmente um modelo de gatilho e simular sua operação em detalhes. Inclusive considerando as propriedades de elementos lógicos reais que possuem apenas um tipo inercial de atrasos. Bem, se você precisa de uma prova formal das propriedades de um gatilho, ela é dada em [5].

Literatura
1. ., ., . UML. . . : , 2007. – 493 .
2. ., ., . : . . – .: , 1988. – 309 .
3. . [ ], : habr.com/ru/post/484588 . . . ( 07.01.2020).
4. . . 2- . – .: , 2004. – 432.
5. .. . [ ], : cloud.mail.ru/public/HwsK/T95PMM8Ed . . . ( 01.02.2020).

All Articles